CF-USDT-Gateway 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": "CF-USDT-Gateway",
"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" }
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 USDT (must be > 0) |
chain | No | string | "eth" or "tron". If omitted, customer chooses on payment page. Auto-selected if only one chain configured. |
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.001234",
"address": "0x...",
"payment_url": "https://pay.kyc.rip/pay/uuid?network=mainnet",
"deep_link": "ethereum:0xdAC17F.../transfer?address=0x...&uint256=50001234",
"qr_data": "0x...?amount=50.001234",
"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.001234",
"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
}
}
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",
"request_id": "payout-batch-001"
}
| Field | Required | Type | Description |
|---|---|---|---|
recipients | Yes | array | List of { address, amount, memo? } |
chain | Yes | string | "eth" or "tron" |
request_id | No | string | Idempotency key. Duplicate request_id returns the existing task. |
CSV Upload: Send Content-Type: text/csv with ?chain=eth query param.
address,amount,memo
0xABC...,100.00,Salary
0xDEF...,50.00,Bonus
Response (201) -- Immediate mode:
{
"task_id": "uuid",
"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.
X-Merchant-Key (payout.create)
Request Body:
{
"payouts": [
{ "chain": "eth", "address": "0x...", "amount": 100.0, "memo": "ETH payout" },
{ "chain": "tron", "address": "T...", "amount": 50.0 }
]
}
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",
"chain": "eth",
"total_amount": 150.0,
"fee_percentage": 0.5,
"fixed_fee": 0.5,
"fee_deducted": 1.25,
"recipients": [
{ "address": "0x...", "amount": 100.0, "memo": "Salary" },
{ "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.001234",
"pay_address": "0x...",
"chain": "eth",
"expires_at": 1711001800,
"payment_url": "https://pay.kyc.rip/pay/dep_uuid?network=mainnet",
"message": "Send exactly 100.001234 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.001234",
"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)
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.completed | Payout task executed on-chain |
Example Payload:
{
"event": "invoice.confirmed",
"invoice_id": "uuid",
"merchant_id": "uuid",
"chain": "eth",
"amount": 50.0,
"adjusted_amount": "50.001234",
"status": "confirmed",
"tx_hash": "0x...",
"confirmations": 12,
"required_confirmations": 12,
"payer_address": "0x...",
"timestamp": 1711001200
}
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));
}
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.
X-Merchant-Key
Response:
{
"ok": true,
"event": "invoice.confirmed",
"status": 200,
"response_body": "OK",
"error": null
}
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.001234",
"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.