uPay.rip API Documentation
Accept USDT payments on Ethereum and Tron. No KYC. Deployed on Cloudflare Workers.
Base URLs
| Environment | URL |
|---|---|
| Mainnet | https://api.pay.kyc.rip |
| Testnet | https://testnet.pay.kyc.rip |
Authentication
All merchant endpoints require the X-Merchant-Key header.
X-Merchant-Key: mk_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Two key types are supported:
| Prefix | Type | Description |
|---|---|---|
mk_ | Master Key | Full admin access. Issued on registration. |
sk_ | Sub-Key | Role-based access (admin, finance, developer, support, payout). |
Request Signing (optional)
When require_signature is enabled on the merchant, all API requests (except from dashboard origins) must include an X-Signature header:
- GET/DELETE: HMAC-SHA256 of sorted query params (
key=value&key=value) - POST/PUT: HMAC-SHA256 of the raw JSON body
X-Signature: sha256=<hex-encoded-hmac>
The HMAC key is the webhook_secret returned at registration.
IP Whitelisting (optional)
Merchants can configure ip_whitelist in settings. When set, only listed IPs may call the API.
Roles & Permissions
| Role | Permissions |
|---|---|
admin | Full access (same as master key) |
finance | Read invoices, deposits, payouts, stats. No settings/keys. |
developer | Create invoices, manage webhooks. No payouts. |
support | Read-only invoices and payouts. |
payout | Create and read payouts only. |
Health Check
GET /
Returns service info.
No Auth
Response:
{
"name": "uPay.rip",
"version": "1.0.0",
"status": "ok",
"environment": "mainnet"
}
GET /health
No Auth
Response:
{ "ok": true }
Merchant
POST /merchant/register
Register a new merchant account. No identity required. Rate limited to 1 per IP per hour.
No Auth
Request Body: None required.
Response (201):
{
"merchant_id": "uuid",
"merchant_key": "mk_uuid",
"webhook_secret": "hmac-hex-string",
"message": "Store your merchant_key securely -- it will not be shown again."
}
Example:
curl -X POST https://api.pay.kyc.rip/merchant/register
GET /merchant/profile
Get current merchant profile and settings.
X-Merchant-Key (any role)
Response:
{
"id": "uuid",
"eth_address": "0x...",
"tron_address": "T...",
"webhook_url": "https://example.com/webhook",
"telegram_chat_id": "123456",
"fee_percentage": 0.5,
"fixed_batch_fee": 0.5,
"invoice_fee_pct": 0.5,
"payout_fee_pct": 0.5,
"payout_fixed_fee": 0.5,
"eth_confirmations": 12,
"tron_confirmations": 20,
"quota_balance": 100.0,
"retention_days": 90,
"ip_whitelist": [],
"require_signature": false,
"payout_auto": false,
"payout_interval": 5,
"created_at": 1711000000,
"environment": "mainnet",
"role": "admin",
"is_sub_key": false
}
Example:
curl -H "X-Merchant-Key: mk_your-key" \
https://api.pay.kyc.rip/merchant/profile
PUT /merchant/settings
Update merchant settings. Only provided fields are updated.
X-Merchant-Key (settings.write)
Request Body:
{
"eth_address": "0x...",
"tron_address": "T...",
"webhook_url": "https://example.com/webhook",
"telegram_chat_id": "123456",
"eth_confirmations": 12,
"tron_confirmations": 20,
"retention_days": 90,
"ip_whitelist": ["1.2.3.4", "5.6.7.8"],
"require_signature": true,
"payout_auto": true,
"payout_interval": 5
}
All fields are optional. Constraints:
| Field | Values |
|---|---|
eth_confirmations | 1-100 |
tron_confirmations | 1-100 |
retention_days | 30, 60, 90, or 180 |
payout_interval | 1, 3, 5, 10, 15, 30, or 60 minutes |
Response:
{ "ok": true }
Example:
curl -X PUT -H "X-Merchant-Key: mk_your-key" \
-H "Content-Type: application/json" \
-d '{"webhook_url":"https://example.com/hook","eth_address":"0xABC..."}' \
https://api.pay.kyc.rip/merchant/settings
GET /merchant/quota
Get current quota balance and fee rates.
X-Merchant-Key
Response:
{
"quota_balance": 100.0,
"fee_percentage": 0.5,
"fixed_batch_fee": 0.5,
"invoice_fee_pct": 0.5,
"payout_fee_pct": 0.5,
"payout_fixed_fee": 0.5
}
GET /merchant/stats
Dashboard statistics: 24h volume, active invoices, daily chart, payout stats.
X-Merchant-Key
Response:
{
"total_received_24h": 1500.0,
"active_invoices": 3,
"quota_balance": 100.0,
"total_fees_paid": 25.5,
"daily_volume": [
{ "date": "2026-03-19", "amount": 200.0 },
{ "date": "2026-03-20", "amount": 350.0 }
],
"recent_invoices": [
{
"id": "uuid",
"chain": "eth",
"amount": 100.0,
"status": "confirmed",
"order_id": "order-123",
"tx_hash": "0x...",
"created_at": 1711000000,
"confirmed_at": 1711001000
}
],
"total_paid_out": 5000.0,
"pending_payouts": 1,
"completed_payouts": 12,
"queued_payouts": 3
}
GET /merchant/fee-history
List fee deductions from confirmed invoices and completed payouts.
X-Merchant-Key
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number |
limit | int | 20 | Items per page (max 100) |
Response:
{
"fees": [
{
"id": "uuid",
"type": "invoice",
"chain": "eth",
"amount": 100.0,
"fee_deducted": 0.5,
"status": "confirmed",
"created_at": 1711000000,
"confirmed_at": 1711001000
}
],
"total_fees_paid": 25.5,
"pagination": { "page": 1, "limit": 20, "total": 42, "pages": 3 }
}
GET /merchant/logs
Audit log for the merchant's account.
X-Merchant-Key
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
action | string | -- | Filter by action (e.g. invoice_created, settings_update) |
page | int | 1 | Page number |
limit | int | 20 | Items per page (max 100) |
Response:
{
"logs": [
{
"id": 1,
"merchant_id": "uuid",
"action": "invoice_created",
"details": "{...}",
"ip": "1.2.3.4",
"created_at": 1711000000
}
],
"total": 100
}
POST /merchant/api-key
Regenerate the master merchant key. The old key is invalidated immediately.
X-Merchant-Key (apikey.regenerate)
Request Body: None.
Response:
{
"merchant_key": "mk_new-uuid",
"message": "New key generated. Previous key is now invalid."
}
POST /merchant/sub-keys
Create a role-scoped sub-key.
X-Merchant-Key (subkey.manage)
Request Body:
{
"role": "finance",
"label": "Accounting system"
}
| Field | Required | Description |
|---|---|---|
role | Yes | One of: admin, finance, developer, support, payout |
label | No | Human-readable label |
Response (201):
{
"sub_key": "sk_uuid",
"role": "finance",
"label": "Accounting system",
"message": "Store this sub-key securely -- it will not be shown again."
}
GET /merchant/sub-keys
List all sub-keys for the merchant.
X-Merchant-Key (subkey.manage)
Response:
{
"sub_keys": [
{
"id": "uuid",
"role": "finance",
"label": "Accounting system",
"created_at": 1711000000,
"last_used_at": 1711005000,
"active": true
}
]
}
DELETE /merchant/sub-keys/:id
Revoke a sub-key. Revoked keys cannot authenticate.
X-Merchant-Key (subkey.manage)
Response:
{ "ok": true, "message": "Sub-key revoked" }
Address Pool
Optionally manage multiple receiving addresses per chain. Invoices are automatically distributed across active addresses using the configured strategy, reducing amount collisions on high-volume merchants.
This feature is backward compatible — merchants using a single address via PUT /merchant/settings continue to work unchanged.
GET /merchant/addresses
List all addresses in the pool.
X-Merchant-Key
Response:
{
"addresses": [
{
"id": "uuid",
"chain": "eth",
"address": "0x...",
"weight": 1,
"active": true,
"pending_invoices": 2,
"created_at": 1711000000
},
{
"id": "uuid",
"chain": "tron",
"address": "T...",
"weight": 2,
"active": true,
"pending_invoices": 0,
"created_at": 1711000000
}
],
"strategy": "least_active"
}
Example:
curl -H "X-Merchant-Key: mk_your-key" \
https://api.pay.kyc.rip/merchant/addresses
POST /merchant/addresses
Add an address to the pool.
X-Merchant-Key (admin)
Request Body:
{
"chain": "eth",
"address": "0x...",
"weight": 1 // optional, default 1
}
| Field | Required | Type | Description |
|---|---|---|---|
chain | Yes | string | "eth" or "tron" |
address | Yes | string | Wallet address for the specified chain |
weight | No | number | Weight for weighted distribution (default 1) |
Response (201):
{
"id": "uuid",
"chain": "eth",
"address": "0x...",
"weight": 1,
"active": true,
"pending_invoices": 0,
"created_at": 1711000000
}
Error Responses:
| Code | Description |
|---|---|
| 409 | Duplicate — address already exists in the pool |
| 400 | Invalid chain or address format |
Example:
curl -X POST -H "X-Merchant-Key: mk_your-key" \
-H "Content-Type: application/json" \
-d '{"chain":"eth","address":"0xABC...","weight":1}' \
https://api.pay.kyc.rip/merchant/addresses
PUT /merchant/addresses/:id
Update the weight or active status of a pool address.
X-Merchant-Key (admin)
Request Body:
{
"weight": 3,
"active": true
}
| Field | Required | Type | Description |
|---|---|---|---|
weight | No | number | New weight value for weighted distribution |
active | No | boolean | Enable or disable the address |
Response:
{ "ok": true }
Example:
curl -X PUT -H "X-Merchant-Key: mk_your-key" \
-H "Content-Type: application/json" \
-d '{"weight":3,"active":true}' \
https://api.pay.kyc.rip/merchant/addresses/ADDRESS_ID
DELETE /merchant/addresses/:id
Remove an address from the pool.
X-Merchant-Key (admin)
Constraints:
- Cannot remove the last active address for a chain
- Cannot remove an address that has pending invoices
Response:
{ "ok": true, "message": "Address removed" }
Error Responses:
| Code | Description |
|---|---|
| 409 | Cannot remove — address has pending invoices |
| 400 | Cannot remove the last active address for this chain |
Example:
curl -X DELETE -H "X-Merchant-Key: mk_your-key" \
https://api.pay.kyc.rip/merchant/addresses/ADDRESS_ID
PUT /merchant/address-strategy
Set the distribution strategy for assigning addresses to new invoices.
X-Merchant-Key (admin)
Request Body:
{
"strategy": "least_active"
}
| Strategy | Description |
|---|---|
least_active | Assigns to the address with the fewest pending invoices (default) |
round_robin | Even distribution across addresses in order |
weighted | Random selection based on each address's weight value |
Response:
{ "ok": true, "strategy": "least_active" }
Example:
curl -X PUT -H "X-Merchant-Key: mk_your-key" \
-H "Content-Type: application/json" \
-d '{"strategy":"least_active"}' \
https://api.pay.kyc.rip/merchant/address-strategy
Invoices
POST /invoice/create
Create a new payment invoice. The fee is deducted from quota immediately.
X-Merchant-Key (invoice.create)
Request Body:
{
"amount": 50.0,
"chain": "eth",
"description": "Order #123",
"expiry_minutes": 30,
"order_id": "order-123"
}
| Field | Required | Type | Description |
|---|---|---|---|
amount | Yes | number | Amount in stablecoin (must be > 0) |
chain | No | string | "eth" or "tron". If omitted, customer chooses on payment page. Auto-selected if only one chain configured. |
token | No | string | "usdt" (default) or "usdc". Must be in merchant's accepted_tokens. |
description | No | string | Invoice description |
expiry_minutes | No | int | Expiry time in minutes (default 30) |
order_id | No | string | Your order reference. Enables idempotency -- duplicate order_id returns the existing invoice. |
Response (201):
{
"invoice_id": "uuid",
"order_id": "order-123",
"chain": "eth",
"available_chains": null,
"amount": 50.0,
"adjusted_amount": "50.001",
"address": "0x...",
"pay_address": "0x...", // pool address assigned to this invoice
"payment_url": "https://pay.kyc.rip/pay/uuid?network=mainnet",
"deep_link": "ethereum:0xdAC17F.../transfer?address=0x...&uint256=50001000",
"qr_data": "0x...?amount=50.001",
"fee_deducted": 0.25,
"required_confirmations": 12,
"expires_at": 1711001800,
"status": "pending"
}
When chain is omitted and the merchant has both ETH and Tron addresses, the response includes available_chains instead:
{
"invoice_id": "uuid",
"chain": null,
"available_chains": ["eth", "tron"],
"address": null,
"deep_link": null,
"qr_data": null,
"..."
}
Error Responses:
| Code | Description |
|---|---|
| 400 | Invalid amount, chain, or no wallet configured |
| 402 | Insufficient quota balance |
Example:
curl -X POST -H "X-Merchant-Key: mk_your-key" \
-H "Content-Type: application/json" \
-d '{"amount":50,"chain":"eth","order_id":"order-123"}' \
https://api.pay.kyc.rip/invoice/create
GET /invoice/:id
Get invoice details (scoped to your merchant).
X-Merchant-Key
Response:
{
"id": "uuid",
"merchant_id": "uuid",
"chain": "eth",
"amount": 50.0,
"adjusted_amount": "50.001",
"description": "Order #123",
"status": "confirmed",
"tx_hash": "0x...",
"confirmations": 12,
"required_confirmations": 12,
"fee_deducted": 0.25,
"payer_address": "0x...",
"order_id": "order-123",
"expires_at": 1711001800,
"confirmed_at": 1711001200,
"created_at": 1711000000
}
Invoice Statuses: pending | detected | confirming | confirmed | expired | failed
GET /invoice/by-order/:orderId
Look up an invoice by your order_id.
X-Merchant-Key
Response: Same as GET /invoice/:id.
Example:
curl -H "X-Merchant-Key: mk_your-key" \
https://api.pay.kyc.rip/invoice/by-order/order-123
GET /invoices
List invoices with pagination and filtering.
X-Merchant-Key
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
status | string | -- | Filter by status |
chain | string | -- | Filter by chain (eth or tron) |
page | int | 1 | Page number |
limit | int | 20 | Items per page (max 100) |
Response:
{
"invoices": [ ... ],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"pages": 8
}
}
POST /invoice/:id/mark-paid
Manually confirm an invoice by providing an on-chain transaction hash. Used by support when auto-detection fails (e.g. customer paid a different amount or from an unmonitored address).
X-Api-Key (invoice.mark_paid) — admin, support roles
Request Body:
{
"tx_hash": "0xabc123..."
}
| Field | Required | Type | Description |
|---|---|---|---|
tx_hash | Yes | string | On-chain transaction hash to verify |
Verification checks:
- Transaction exists on-chain and is confirmed
- Transaction is a USDT transfer on the correct chain
- Recipient address matches the invoice payment address
- Transaction has not already been used for another invoice
- Invoice is in a markable state (pending, detected, or expired)
Response (200):
{
"ok": true,
"invoice_id": "uuid",
"status": "confirmed",
"tx_hash": "0xabc123...",
"paid_amount": "48.50",
"adjusted_amount": "50.001",
"marked_by": "support@example.com"
}
Error Responses:
| Code | Description |
|---|---|
| 400 | Missing tx_hash or invalid format |
| 404 | Invoice not found |
| 409 | Invoice already confirmed |
| 422 | Transaction verification failed (wrong chain, wrong recipient, already used, or not confirmed) |
When successful, a webhook is fired with event invoice.confirmed. If the transaction amount differs from adjusted_amount, the webhook payload includes a paid_amount field.
Payouts
Payouts support two modes:
- Immediate mode (
payout_auto = false): A payout task is created immediately and pushed to the Vault for signing. - Batched mode (
payout_auto = true): Recipients are queued and batched at intervals defined bypayout_interval.
POST /payout/create
Create a payout task with one or more recipients on a single chain.
X-Merchant-Key (payout.create)
Request Body (JSON):
{
"recipients": [
{ "address": "0x...", "amount": 100.0, "memo": "Salary" },
{ "address": "0x...", "amount": 50.0 }
],
"chain": "eth",
"token": "usdt",
"request_id": "payout-batch-001",
"order_id": "internal-ref-xyz"
}
| Field | Required | Type | Description |
|---|---|---|---|
recipients | Yes | array | List of { address, amount, memo? } |
chain | Yes | string | "eth" or "tron" |
token | No | string | "usdt" (default) or "usdc". USDC is Ethereum-only. |
request_id | No | string | Idempotency key. Duplicate request_id returns the existing task. |
order_id | No | string | Your internal correlation id (order number, invoice ref, etc.). Passed through unchanged to all payout webhook events. Not unique — distinct from request_id. |
CSV Upload: Send Content-Type: text/csv with ?chain=eth query param. Optional: ?order_id=your-ref and ?request_id=unique-key.
address,amount,memo
0xABC...,100.00,Salary
0xDEF...,50.00,Bonus
Response (201) -- Immediate mode:
{
"task_id": "uuid",
"order_id": "internal-ref-xyz",
"fee_estimate": 1.25,
"total_amount": 150.0,
"recipients_count": 2,
"chain": "eth",
"status": "pending"
}
Response (202) -- Batched mode:
{
"queued": true,
"recipients_count": 2,
"total_amount": 150.0,
"fee_estimate": 1.25,
"batch_interval_min": 5,
"message": "2 recipients queued. Will be batched within 5 minute(s)."
}
Error Responses:
| Code | Description |
|---|---|
| 400 | Invalid chain, address format, or amount |
| 402 | Insufficient quota balance for estimated fee |
POST /payout/batch
Submit multiple payouts with mixed chains. Each item has its own chain, address, and amount. Recipients on the same chain are grouped into a single task.
X-Merchant-Key (payout.create)
Request Body:
{
"order_id": "batch-ref-xyz",
"payouts": [
{ "chain": "eth", "address": "0x...", "amount": 100.0, "memo": "ETH payout", "order_id": "leg-1" },
{ "chain": "tron", "address": "T...", "amount": 50.0, "order_id": "leg-2" }
]
}
| Field | Required | Type | Description |
|---|---|---|---|
payouts | Yes | array | List of { chain, address, amount, memo?, order_id? }. Recipients on the same chain are grouped into a single task. |
order_id | No | string | Batch-level correlation id. Applied to every synthesized task (one per chain). Included in all payout webhook events. |
payouts[].order_id | No | string | Per-recipient correlation id. Preserved inside recipients_json on the resulting task; useful when a multi-recipient batch needs per-leg tracking in addition to (or instead of) the batch-level id. |
Response (201) -- Immediate mode:
{
"task_ids": ["uuid-eth", "uuid-tron"],
"total_amount": 150.0,
"fee_estimate": 1.25,
"count": 2
}
Response (202) -- Batched mode:
{
"queued": true,
"count": 2,
"total_amount": 150.0,
"fee_estimate": 1.25,
"batch_interval_min": 5,
"message": "2 payouts queued."
}
GET /payout/:id
Get payout task details.
X-Merchant-Key
Response:
{
"id": "uuid",
"merchant_id": "uuid",
"request_id": "payout-batch-001",
"order_id": "internal-ref-xyz",
"chain": "eth",
"token": "usdt",
"total_amount": 150.0,
"fee_percentage": 0.5,
"fixed_fee": 0.5,
"fee_deducted": 1.25,
"recipients": [
{ "address": "0x...", "amount": 100.0, "memo": "Salary", "order_id": "leg-1" },
{ "address": "0x...", "amount": 50.0 }
],
"recipients_count": 2,
"status": "completed",
"tx_hash": "0x...",
"challenge": null,
"created_at": 1711000000,
"confirmed_at": 1711000100,
"executed_at": 1711000200
}
Payout Statuses: pending | confirmed | executing | completed | cancelled | failed
GET /payout/queue
View queued payout items waiting for the next batch.
X-Merchant-Key
Response:
{
"queued": [
{
"id": 1,
"chain": "eth",
"address": "0x...",
"amount": 100.0,
"memo": null,
"created_at": 1711000000
}
],
"count": 1,
"total_amount": 100.0,
"batch_interval_min": 5,
"payout_auto": true
}
GET /payouts
List payout tasks with pagination.
X-Merchant-Key
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
status | string | -- | Filter by status |
page | int | 1 | Page number |
limit | int | 20 | Items per page (max 100) |
Response:
{
"payouts": [ ... ],
"queued_count": 3,
"pagination": { "page": 1, "limit": 20, "total": 50, "pages": 3 }
}
POST /payout/:id/confirm
Two-step challenge-response confirmation for a pending payout task.
X-Merchant-Key
Step 1 -- Request challenge (empty body or {}):
curl -X POST -H "X-Merchant-Key: mk_your-key" \
-H "Content-Type: application/json" \
-d '{}' \
https://api.pay.kyc.rip/payout/TASK_ID/confirm
Response:
{
"challenge": "uuid-challenge-string",
"task_id": "uuid"
}
Step 2 -- Confirm with challenge response:
curl -X POST -H "X-Merchant-Key: mk_your-key" \
-H "Content-Type: application/json" \
-d '{"challenge_response":"uuid-challenge-string"}' \
https://api.pay.kyc.rip/payout/TASK_ID/confirm
Response:
{
"task_id": "uuid",
"status": "confirmed"
}
POST /payout/:id/report
Report the on-chain transaction hash after the Vault executes the payout. The fee is deducted from quota at this point.
X-Merchant-Key
Request Body:
{
"tx_hash": "0x..."
}
Response:
{
"task_id": "uuid",
"status": "completed",
"tx_hash": "0x...",
"fee_deducted": 1.25
}
Error Responses:
| Code | Description |
|---|---|
| 400 | Task is not in confirmed status |
| 402 | Insufficient quota balance to cover fee |
POST /payout/:id/cancel
Cancel a pending or confirmed payout task (before execution).
X-Merchant-Key
Request Body: None.
Response:
{
"task_id": "uuid",
"status": "cancelled"
}
Deposits
Deposits allow merchants to add quota balance by sending USDT to the platform fee wallet.
POST /deposit/create
Create a deposit intent. Returns a unique adjusted amount to send.
X-Merchant-Key
Request Body:
{
"amount": 100.0,
"chain": "eth"
}
| Field | Required | Type | Description |
|---|---|---|---|
amount | Yes | number | Deposit amount in USDT |
chain | Yes | string | "eth" or "tron" |
Response (201):
{
"deposit_id": "uuid",
"amount": 100.0,
"adjusted_amount": "100.001",
"pay_address": "0x...",
"chain": "eth",
"expires_at": 1711001800,
"payment_url": "https://pay.kyc.rip/pay/dep_uuid?network=mainnet",
"message": "Send exactly 100.001 USDT to the address below."
}
If a pending deposit already exists for the same chain, it returns the existing one instead of creating a duplicate.
GET /deposit/:id
Check deposit status.
X-Merchant-Key
Response:
{
"id": "uuid",
"merchant_id": "uuid",
"chain": "eth",
"amount": 100.0,
"adjusted_amount": "100.001",
"tx_hash": "0x...",
"pay_address": "0x...",
"status": "confirmed",
"expires_at": 1711001800,
"confirmed_at": 1711001200,
"created_at": 1711000000
}
Deposit Statuses: pending | confirmed | expired
GET /deposits
List deposit history (most recent 50).
X-Merchant-Key
Response:
{
"deposits": [ ... ]
}
Webhooks
Webhooks deliver real-time event notifications to your configured webhook_url.
Webhook Payload
All webhooks are POST requests with:
Content-Type: application/jsonX-Webhook-Signature: HMAC-SHA256 hex signature of the body, using yourwebhook_secretX-Webhook-Event: Event name (e.g.invoice.confirmed,payout.completed)X-Webhook-Attempt: Delivery attempt number (1on first try, increments on retries)X-Webhook-Timestamp: Unix seconds at dispatch time
Invoice events:
| Event | Description |
|---|---|
invoice.detected | Payment transaction detected on-chain |
invoice.confirming | Transaction has some confirmations but not enough |
invoice.confirmed | Invoice fully confirmed |
invoice.expired | Invoice expired without payment |
Payout events:
| Event | Description |
|---|---|
payout.created | Payout task queued for signing (initial state) |
payout.confirmed | Vault has signed the transaction and is ready to broadcast |
payout.executing | Vault acquired execution lock — signing in progress |
payout.completed | Payout task executed on-chain with a final tx_hash |
payout.cancelled | Payout task cancelled before execution |
Wildcard event filters supported on /webhook/endpoints: invoice.*, payout.*, *.
Invoice Event Payload Example:
{
"event": "invoice.confirmed",
"invoice_id": "uuid",
"merchant_id": "uuid",
"order_id": "your-internal-ref", // from invoice create body
"chain": "eth",
"token": "usdt",
"token_address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"amount": 50.0,
"adjusted_amount": "50.001",
"paid_amount": "48.50", // only present if differs from adjusted_amount
"status": "confirmed",
"tx_hash": "0x...",
"confirmations": 12,
"required_confirmations": 12,
"payer_address": "0x...",
"timestamp": 1711001200
}
Payout Event Payload Example:
{
"event": "payout.completed",
"task_id": "uuid",
"merchant_id": "uuid",
"order_id": "internal-ref-xyz", // from payout create body
"chain": "eth",
"token": "usdt",
"amount": 150.0,
"recipients": [
{ "address": "0xABC...", "amount": 100.0, "memo": "Invoice #42" },
{ "address": "0xDEF...", "amount": 50.0 }
],
"recipients_count": 2,
"tx_hash": "0x...",
"fee_deducted": 1.25,
"timestamp": 1711001200
}
Notes:
paid_amountis only present on invoice events when the actual on-chain payment differs fromadjusted_amount(e.g. manual mark-paid by support). Normal auto-confirmed invoices omit this field.order_idis pass-through and optional — only present if you set it when creating the invoice or payout.tokenis"usdt"or"usdc". Legacy invoices from before multi-token support default to"usdt".- Payout callbacks fire per
order_id— batched tasks produce one callback per distinct order. Therecipientsarray includes each recipient'saddress,amount, and optionalmemo. - Payout events for tasks cancelled before completion also include a
reasonstring. - Manually-resent webhooks include
"resend": true.
Verifying Webhooks
Python:
import hmac, hashlib
def verify(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
JavaScript:
const crypto = require('crypto');
function verify(body, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(body).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
GET /webhook/source-ips
Public endpoint — no authentication required. Returns the current list of source IP addresses from which uPay dispatches webhooks. Merchants can allowlist these in their firewall as a second layer of defense alongside HMAC signature verification — so that even if a signature key leaks, requests from any other source IP are rejected at the network boundary.
Public — cached 5 min at the edge.
Response:
{
"ips": ["194.233.89.217", "194.233.88.244"],
"note": "All uPay webhooks are sent from one of these IPs...",
"updated_at": "2026-04-11"
}
Usage recommendations:
- Fetch this endpoint at deploy time or periodically (e.g. daily cron) and reconcile your firewall allowlist with the returned IPs. The set may expand for redundancy.
- Both HMAC signature verification AND IP allowlisting are recommended. Use signature verification for request authenticity and IP allowlisting as a network-level guardrail.
- All production webhook traffic goes through this static-IP pool — not only live events but also test-webhook dispatches from the dashboard. So the "test" button you click in the merchant portal hits your server from the same IP as real webhooks.
Example:
curl https://api.pay.kyc.rip/webhook/source-ips
POST /webhook/test
Send a test webhook to your configured URL.
X-Merchant-Key
Request Body: None.
Response:
{
"ok": true,
"status": 200,
"error": null
}
POST /webhook/resend/:invoiceId
Re-trigger the webhook for a specific invoice. Emits the event matching the invoice's current status (invoice.confirmed, invoice.expired, etc.) to all configured endpoints whose event filter matches. Payload includes "resend": true.
X-Merchant-Key
Response:
{
"ok": true,
"event": "invoice.confirmed",
"enqueued": 2
}
POST /webhook/resend-payout/:taskId
Re-trigger the payout webhook for a specific task. Emits payout.completed (or payout.{status} for other statuses) to all configured endpoints whose event filter matches. Payload mirrors the original dispatch shape including order_id, chain, token, amount, recipients, recipients_count, tx_hash, fee_deducted — plus "resend": true.
X-Merchant-Key
Response:
{
"ok": true,
"event": "payout.completed",
"enqueued": 2
}
Returns 400 if no webhook endpoint is configured to receive the event (check your endpoint's events filter includes payout.* or the specific event).
GET /webhook/logs
View webhook delivery logs.
X-Merchant-Key
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number |
limit | int | 20 | Items per page (max 100) |
Response:
{
"logs": [
{
"id": "uuid",
"merchant_id": "uuid",
"invoice_id": "uuid",
"url": "https://example.com/webhook",
"status": 200,
"attempts": 1,
"last_error": null,
"payload": "{...}",
"response_body": "OK",
"created_at": 1711000000
}
],
"pagination": { "page": 1, "limit": 20, "total": 10, "pages": 1 }
}
Public Endpoints
These endpoints require no authentication. They are used by the payment page UI.
GET /public/invoice/:id
Get invoice status for the payment page. Returns limited data (no merchant secrets).
No Auth
Response:
{
"id": "uuid",
"chain": "eth",
"amount": 50.0,
"adjusted_amount": "50.001",
"address": "0x...",
"status": "pending",
"confirmations": 0,
"required_confirmations": 12,
"expires_at": 1711001800,
"created_at": 1711000000
}
When the invoice has no chain selected yet:
{
"id": "uuid",
"chain": null,
"available_chains": ["eth", "tron"],
"address": null,
"..."
}
Also supports deposit intents with dep_ prefix (e.g. /public/invoice/dep_uuid).
POST /public/invoice/:id/select-chain
Customer selects which chain to pay on (for invoices created without a chain).
No Auth
Request Body:
{
"chain": "tron"
}
Response:
{
"ok": true,
"chain": "tron",
"address": "T...",
"required_confirmations": 20
}
Errors:
- 400 if chain already selected or invoice not pending
- 404 if invoice not found
WebSocket (Vault)
GET /ws/vault
WebSocket endpoint for the Vault desktop client. Receives real-time payout tasks and broadcasts events.
X-Merchant-Key (via query param ?key=mk_...)
Upgrade: WebSocket
Events received:
payout.created-- new task ready for signingpayout.confirmed-- task confirmed via challengepayout.completed-- task executed (tx reported)payout.cancelled-- task cancelled
Error Responses
All errors follow a consistent format:
{
"error": "Human-readable error message"
}
Common HTTP Status Codes:
| Code | Meaning |
|---|---|
| 400 | Bad Request -- invalid input |
| 401 | Unauthorized -- missing or invalid key |
| 402 | Payment Required -- insufficient quota |
| 403 | Forbidden -- IP not whitelisted or invalid signature |
| 404 | Not Found |
| 409 | Conflict -- duplicate wallet address |
| 429 | Rate Limited |
| 500 | Internal Server Error |
Rate Limits
| Endpoint | Limit |
|---|---|
POST /merchant/register | 1 per IP per hour |
| All other endpoints | No hard rate limit (Cloudflare WAF applies) |
Data Retention
Merchants can configure retention_days (30, 60, 90, or 180 days). Expired data is purged daily at 03:00 UTC. Immediate purge available via POST /merchant/purge-now.