Issue invoices, read balances, dispatch withdrawals, receive durable webhooks. Every endpoint authenticated by your API key.
Every request — GET or POST — must carry three headers. Missing or invalid signatures return 401 UNAUTHORIZED.
X-API-Key: xpay_xxxxxxxxxxxxxxxxxxxxxxxxxxxx X-Timestamp: 1711324800 X-Signature: <hex hmac-sha256>
msg = f"{timestamp}\n{method}\n{path}\n".encode() + body
sig = hmac_sha256(secret, msg).hex()import hmac, hashlib, time, json, requests KEY_ID = "xpay_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" SECRET = "your-secret-shown-once" BASE = "https://xthonpay.com" def signed(method, path, payload=None): body = b"" if payload is None else json.dumps(payload, separators=(",",":")).encode() ts = str(int(time.time())) msg = f"{ts}\n{method}\n{path}\n".encode() + body sig = hmac.new(SECRET.encode(), msg, hashlib.sha256).hexdigest() headers = { "X-API-Key": KEY_ID, "X-Timestamp": ts, "X-Signature": sig, "Content-Type": "application/json", } return requests.request(method, BASE + path, data=body, headers=headers) print(signed("GET", "/v1/balance").json())
All paths below are absolute under that host. For example, /v1/balance means https://xthonpay.com/v1/balance.
Each API key carries scopes, a per-minute rate limit, and optional guard-rails. Tune these from the bot or via SQL. Max request body size is 64 KiB.
| Setting | Default | Notes |
|---|---|---|
| permissions | ["read"] | Values: read, write, withdraw. New keys are read-only until upgraded. |
| rate_limit | 60 / min | Per-key. Exceeding returns 429 RATE_LIMITED. |
| daily_withdraw_cap | 0 USDT | Must be > 0 or withdrawals fail with 403 WITHDRAW_NOT_PERMITTED. |
| withdraw_addresses | [] | Empty = unrestricted. Non-empty = only those addresses. |
| ip_whitelist | [] | Empty = any IP. Non-empty = enforced against X-Real-IP. |
All error responses share this shape. 500s include request_id for support lookups.
{
"error": {
"code": "ERROR_CODE",
"message": "human-readable reason"
}
}| HTTP | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing/invalid API key, signature, or timestamp drift > 300s. |
| 401 | KEY_REGENERATION_REQUIRED | Legacy key without encrypted secret — recreate it. |
| 403 | FORBIDDEN | Key lacks the required permission. |
| 403 | WITHDRAW_NOT_PERMITTED | daily_withdraw_cap = 0. |
| 403 | ADDRESS_NOT_ALLOWED | Destination not in the key's allowlist. |
| 403 | DAILY_CAP_EXCEEDED | 24h sum would exceed daily_withdraw_cap. |
| 400 | INVALID_JSON · INVALID_AMOUNT · INVALID_ADDRESS · INVALID_URL | Field validation failed. |
| 400 | INSUFFICIENT_BALANCE | DB balance below requested amount. |
| 429 | RATE_LIMITED | Per-key or per-endpoint limit exceeded. |
| 500 | INTERNAL_ERROR | Transient. Retry with backoff; quote request_id. |
read
Returns the merchant's off-chain ledger balance and the webhook signing secret used to verify incoming webhooks.
{
"balance": "14500.50000000",
"frozen_balance": "0",
"total_deposited": "18000.00000000",
"total_withdrawn": "3499.50000000",
"webhook_secret": "64-char-hex-used-to-verify-webhooks"
}read
Returns the merchant's primary cold HD deposit address on BNB Smart Chain. Direct deposits to this address credit the main balance (separate from per-invoice addresses).
{
"address": "0x7a3B4c2d9E1f...8a6F42d",
"network": "BSC (BEP20)",
"token": "USDT"
}write
Creates an invoice with a unique one-time deposit address. Payments are detected automatically, swept to the merchant, and fire an invoice.paid webhook.
| Name | Type | Description |
|---|---|---|
| amountreq | string | USDT amount. Must be > 0. |
| external_id | string | Your reference ID. Unique per merchant. Max 255 chars. |
| description | string | Up to 1000 chars. Shown in admin panels. |
| callback_url | string | Webhook destination. Must be public HTTPS; private / loopback / reserved IPs rejected. |
| success_url | string | Same validation as callback_url. |
| cancel_url | string | Same validation. |
| metadata | object | JSON blob for your own use. Max 4 KiB. |
| expires_in | integer | Seconds until expiry. Default 1800. |
{
"amount": "100.00",
"external_id": "ORDER-9921",
"description": "Pro subscription (1 month)",
"callback_url": "https://yourapp.com/webhooks/xthonpay",
"success_url": "https://yourapp.com/orders/9921",
"metadata": { "user_id": 4821 },
"expires_in": 1800
}{
"status": "success",
"data": {
"invoice_id": 42,
"deposit_address": "0x9Cb0...bd74",
"amount": "100.00",
"currency": "USDT",
"status": "pending",
"expires_at": "2026-04-15T10:30:00"
}
}read
Check the current status of an invoice.
{
"status": "success",
"data": {
"invoice_id": 42,
"external_id": "ORDER-9921",
"deposit_address": "0x9Cb0...bd74",
"amount": "100.00",
"amount_received": "100.00",
"currency": "USDT",
"status": "paid",
"tx_hash": "0xabc...123",
"from_address": "0x71C7...62b",
"paid_at": "2026-04-15T10:12:00",
"expires_at": "2026-04-15T10:30:00"
}
}status ∈ { pending, paid, expired, cancelled }
withdraw
Send USDT or BNB from the merchant's balance to an external BSC address. The merchant's cold wallet signs the tx on-chain; gas is paid by the merchant (ensure ~0.0005 BNB is present).
| Name | Type | Description |
|---|---|---|
| amountreq | string | Must satisfy MIN_WITHDRAWAL ≤ amount ≤ MAX_WITHDRAWAL. |
| to_addressreq | string | BSC destination. Must match ^0x[0-9a-fA-F]{40}$. |
| currency | string | "USDT" (default) or "BNB". |
{
"amount": "500.00",
"to_address": "0x71C765...9A62b",
"currency": "USDT"
}{
"ok": true,
"withdrawal_id": 87,
"status": "approved",
"currency": "USDT",
"amount": "500.00000000",
"fee": "0.00000000",
"net_amount": "500.00000000",
"to_address": "0x71C7...62b"
}Response is synchronous — the withdrawal is accepted and queued, not yet broadcast. Status progresses approved → processing → completed (or failed, in which case the balance is auto-refunded). Poll /v1/balance or subscribe to withdrawal.completed.
XthonPay POSTs signed JSON to your callback_url. The signing key is your webhook_secret returned by GET /v1/balance.
Content-Type: application/json User-Agent: XthonPay-Webhook/1.0 X-XthonPay-Timestamp: 1711324800 X-XthonPay-Delivery: b4f2a1c8-1234-4abc-9d5f-ff8a1b2c3d4e X-XthonPay-Signature: <hex hmac-sha256>
msg = f"{timestamp}.{delivery_id}.".encode() + raw_body
sig = hmac_sha256(webhook_secret, msg).hex()import hmac, hashlib, time def verify(req, secret: str, tolerance: int = 300) -> bool: ts = req.headers["X-XthonPay-Timestamp"] did = req.headers["X-XthonPay-Delivery"] sig = req.headers["X-XthonPay-Signature"] if abs(time.time() - int(ts)) > tolerance: return False msg = f"{ts}.{did}.".encode() + req.body expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, sig)
Dedupe on X-XthonPay-Delivery — retries of the same event share the UUID.
{
"event": "invoice.paid",
"data": {
"invoice_id": 42,
"external_id": "ORDER-9921",
"amount": "100.00",
"amount_received": "100.00",
"currency": "USDT",
"status": "paid",
"tx_hash": "0xabc...123",
"from_address": "0x71C7...62b",
"paid_at": "2026-04-15T10:12:00"
}
}Our team is available in Telegram, any hour.