04 — Technical

XthonPay REST API

Issue invoices, read balances, dispatch withdrawals, receive durable webhooks. Every endpoint authenticated by your API key.

https://xthonpay.com HMAC-SHA256 signed JSON in · JSON out v1 · BSC Mainnet
01 · Primer
AUTH Authentication

Every request — GET or POST — must carry three headers. Missing or invalid signatures return 401 UNAUTHORIZED.

Required headers

headers
X-API-Key:   xpay_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Timestamp: 1711324800
X-Signature: <hex hmac-sha256>

Signature formula

pseudocode
msg = f"{timestamp}\n{method}\n{path}\n".encode() + body
sig = hmac_sha256(secret, msg).hex()
  • secretthe plaintext shown once when the key was created. Not retrievable; lose it → regenerate.
  • methodHTTP verb in upper case.
  • pathrequest path, no query string (e.g. /v1/invoices).
  • bodyraw request body bytes. Empty bytes for GET.
  • timestampUnix seconds. Requests with drift > 300s are rejected.

Python example

sign_request.py
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())
Legacy keys (created before the HMAC rollout) return KEY_REGENERATION_REQUIRED. Regenerate them in @XthonPayBot → API Keys.
01 · Primer
BASE Base URL

All paths below are absolute under that host. For example, /v1/balance means https://xthonpay.com/v1/balance.

01 · Primer
CONFIG Permissions & Limits

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.

SettingDefaultNotes
permissions["read"]Values: read, write, withdraw. New keys are read-only until upgraded.
rate_limit60 / minPer-key. Exceeding returns 429 RATE_LIMITED.
daily_withdraw_cap0 USDTMust 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.
01 · Primer
ERRORS Errors

All error responses share this shape. 500s include request_id for support lookups.

error envelope
{
  "error": {
    "code":    "ERROR_CODE",
    "message": "human-readable reason"
  }
}

Common codes

HTTPCodeWhen
401UNAUTHORIZEDMissing/invalid API key, signature, or timestamp drift > 300s.
401KEY_REGENERATION_REQUIREDLegacy key without encrypted secret — recreate it.
403FORBIDDENKey lacks the required permission.
403WITHDRAW_NOT_PERMITTEDdaily_withdraw_cap = 0.
403ADDRESS_NOT_ALLOWEDDestination not in the key's allowlist.
403DAILY_CAP_EXCEEDED24h sum would exceed daily_withdraw_cap.
400INVALID_JSON · INVALID_AMOUNT · INVALID_ADDRESS · INVALID_URLField validation failed.
400INSUFFICIENT_BALANCEDB balance below requested amount.
429RATE_LIMITEDPer-key or per-endpoint limit exceeded.
500INTERNAL_ERRORTransient. Retry with backoff; quote request_id.
02 · Wallet
GET /v1/balance read

Returns the merchant's off-chain ledger balance and the webhook signing secret used to verify incoming webhooks.

Response — 200 OK

200 OK
{
  "balance":         "14500.50000000",
  "frozen_balance":  "0",
  "total_deposited": "18000.00000000",
  "total_withdrawn": "3499.50000000",
  "webhook_secret":  "64-char-hex-used-to-verify-webhooks"
}
webhook_secret is shared across all API keys owned by the same merchant. It's the HMAC key for outbound webhooks — see the Webhooks section below.
02 · Wallet
GET /v1/deposit-address 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).

Response — 200 OK

200 OK
{
  "address": "0x7a3B4c2d9E1f...8a6F42d",
  "network": "BSC (BEP20)",
  "token":   "USDT"
}
03 · Payments
POST /v1/invoices 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.

Parameters

NameTypeDescription
amountreqstringUSDT amount. Must be > 0.
external_idstringYour reference ID. Unique per merchant. Max 255 chars.
descriptionstringUp to 1000 chars. Shown in admin panels.
callback_urlstringWebhook destination. Must be public HTTPS; private / loopback / reserved IPs rejected.
success_urlstringSame validation as callback_url.
cancel_urlstringSame validation.
metadataobjectJSON blob for your own use. Max 4 KiB.
expires_inintegerSeconds until expiry. Default 1800.

Request body

POST /v1/invoices
{
  "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
}

Response — 201 Created

201 Created
{
  "status": "success",
  "data": {
    "invoice_id":      42,
    "deposit_address": "0x9Cb0...bd74",
    "amount":          "100.00",
    "currency":        "USDT",
    "status":          "pending",
    "expires_at":      "2026-04-15T10:30:00"
  }
}
On payment the merchant receives amount − 0.01 USDT. A flat 0.01 USDT platform fee is deducted from every successful invoice.
03 · Payments
GET /v1/invoices/{id} read

Check the current status of an invoice.

Response — 200 OK

200 OK
{
  "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 }

02 · Wallet
POST /v1/withdraw 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).

Parameters

NameTypeDescription
amountreqstringMust satisfy MIN_WITHDRAWAL ≤ amount ≤ MAX_WITHDRAWAL.
to_addressreqstringBSC destination. Must match ^0x[0-9a-fA-F]{40}$.
currencystring"USDT" (default) or "BNB".
This endpoint does NOT require a 2FA code. The HMAC-signed API key is the sole authentication factor — protect the secret accordingly.

Request body

POST /v1/withdraw
{
  "amount":     "500.00",
  "to_address": "0x71C765...9A62b",
  "currency":   "USDT"
}

Response — 200 OK

200 OK
{
  "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.

04 · Events
WEBHOOK Webhooks

XthonPay POSTs signed JSON to your callback_url. The signing key is your webhook_secret returned by GET /v1/balance.

Events

  • invoice.paidInvoice fully or over-paid and credited.
  • invoice.expiredInvoice expired without payment.
  • deposit.confirmedDirect deposit credited to merchant balance.
  • withdrawal.completedWithdrawal confirmed on-chain.
  • withdrawal.failedWithdrawal failed and balance refunded.

Request headers

incoming webhook
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>

Signature formula

pseudocode
msg = f"{timestamp}.{delivery_id}.".encode() + raw_body
sig = hmac_sha256(webhook_secret, msg).hex()

Verification example

verify.py
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.

Payload — invoice.paid

POST body
{
  "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"
  }
}
Always verify both the signature and the timestamp freshness. Never trust webhook bodies alone — anyone can POST to a public URL.

Need help integrating?

Our team is available in Telegram, any hour.

Contact Support Integration Guide