CF-USDT-Gateway API Documentation

Accept USDT payments on Ethereum and Tron. No KYC. Deployed on Cloudflare Workers.

Base URLs

EnvironmentURL
Mainnethttps://api.pay.kyc.rip
Testnethttps://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:

PrefixTypeDescription
mk_Master KeyFull admin access. Issued on registration.
sk_Sub-KeyRole-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:

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

RolePermissions
adminFull access (same as master key)
financeRead invoices, deposits, payouts, stats. No settings/keys.
developerCreate invoices, manage webhooks. No payouts.
supportRead-only invoices and payouts.
payoutCreate 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:

FieldValues
eth_confirmations1-100
tron_confirmations1-100
retention_days30, 60, 90, or 180
payout_interval1, 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:

ParamTypeDefaultDescription
pageint1Page number
limitint20Items 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:

ParamTypeDefaultDescription
actionstring--Filter by action (e.g. invoice_created, settings_update)
pageint1Page number
limitint20Items 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"
}
FieldRequiredDescription
roleYesOne of: admin, finance, developer, support, payout
labelNoHuman-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"
}
FieldRequiredTypeDescription
amountYesnumberAmount in USDT (must be > 0)
chainNostring"eth" or "tron". If omitted, customer chooses on payment page. Auto-selected if only one chain configured.
descriptionNostringInvoice description
expiry_minutesNointExpiry time in minutes (default 30)
order_idNostringYour 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:

CodeDescription
400Invalid amount, chain, or no wallet configured
402Insufficient 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:

ParamTypeDefaultDescription
statusstring--Filter by status
chainstring--Filter by chain (eth or tron)
pageint1Page number
limitint20Items per page (max 100)

Response:

{
  "invoices": [ ... ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 150,
    "pages": 8
  }
}

Payouts

Payouts support two modes:

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"
}
FieldRequiredTypeDescription
recipientsYesarrayList of { address, amount, memo? }
chainYesstring"eth" or "tron"
request_idNostringIdempotency 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:

CodeDescription
400Invalid chain, address format, or amount
402Insufficient 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:

ParamTypeDefaultDescription
statusstring--Filter by status
pageint1Page number
limitint20Items 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:

CodeDescription
400Task is not in confirmed status
402Insufficient 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"
}
FieldRequiredTypeDescription
amountYesnumberDeposit amount in USDT
chainYesstring"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:

Events:

EventDescription
invoice.detectedPayment transaction detected on-chain
invoice.confirmingTransaction has some confirmations but not enough
invoice.confirmedInvoice fully confirmed
invoice.expiredInvoice expired without payment
payout.completedPayout 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:

ParamTypeDefaultDescription
pageint1Page number
limitint20Items 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:


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:


Error Responses

All errors follow a consistent format:

{
  "error": "Human-readable error message"
}

Common HTTP Status Codes:

CodeMeaning
400Bad Request -- invalid input
401Unauthorized -- missing or invalid key
402Payment Required -- insufficient quota
403Forbidden -- IP not whitelisted or invalid signature
404Not Found
409Conflict -- duplicate wallet address
429Rate Limited
500Internal Server Error

Rate Limits

EndpointLimit
POST /merchant/register1 per IP per hour
All other endpointsNo 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.