Payout API
ChainPay API for sending crypto to a recipient address (withdrawals). Unlike
deposits (public, no key), payouts move funds on our side, so every
/payout/* call needs the shared x-api-key, and each wallet is additionally
protected by its own walletAccessToken.
Status: coming soon. Payouts are not live yet. The API below describes the planned contract and may change before launch.
Base URL
https://api.chainpay.site
TLS is required; plain HTTP is rejected.
Supported networks
tron, bsc, eth, polygon, arbitrum. solana is not supported.
Model — a dedicated wallet per client, no internal ledger
Every payout wallet is a real HD address on the network. We never keep an internal client balance: the real on-chain balance of the address is read before each withdrawal. You fund that address yourself — this is "send what is actually there", not "withdraw from our account".
The recipient always receives the full requested amount — no fee is taken
from the token. The only real cost is the network cost (Energy/Bandwidth on
TRON, gas on EVM chains), paid entirely separately from your own gas-station
wallet (section 1) and never touching the amount destination receives.
1. POST /payout/wallet — get or create a wallet
Requires x-api-key. Get-or-create: the same
(chain, tokenSymbol, externalReference) always returns the same address.
curl -X POST https://api.chainpay.site/payout/wallet \
-H 'content-type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-d '{
"chain": "tron",
"tokenSymbol": "USDT",
"externalReference": "site-42",
"callbackUrl": "https://your-service.example.com/payout-webhook?token=your-secret"
}'
| Field | Required | Description |
|---|---|---|
chain | yes | tron, bsc, eth, polygon or arbitrum |
tokenSymbol | yes | Token you will withdraw from this wallet (e.g. USDT) |
externalReference | yes | Your own id (site / client / account) |
callbackUrl | yes | Public HTTP(S) URL for payout status webhooks. Must not point at localhost or a private IP. |
Response (201) — first creation
{
"id": "6311badc-787a-4e33-804a-9562a553e320",
"chain": "tron",
"tokenSymbol": "USDT",
"address": "THjTgKDGj3Biar7ZTSmYZAWsqkTKhWGv5Y",
"externalReference": "site-42",
"walletAccessToken": "9f2c8a1e4b3d7f60...(64 hex chars)",
"gasStationAddress": "TDZkhJWMeaFAhefQXK4mG8GtkSZahw13jK"
}
walletAccessToken is shown exactly once. Only its hash is stored. Save it
immediately; every POST /payout/withdraw from this wallet needs it.
gasStationAddress is your own wallet for network resources on that chain.
It is not a withdrawal address: send it some of the chain's native coin
(TRX for tron, ETH/BNB/MATIC for the matching EVM chain — not the token you
withdraw) so the system can fund each of your withdrawals. There is one gas
station per externalReference per chain, shared by all your payout wallets
regardless of token. If it runs short, /payout/withdraw fails (failed) with
no automatic cover from our side; top it up and retry with a new invoiceId.
A repeated call returns the same structure without walletAccessToken
(gasStationAddress is always returned).
Errors (400)
chain, tokenSymbol, externalReference and callbackUrl are required.Invalid callbackUrl: ...— not HTTP(S), or points to a private/local address.Payout not yet supported for chain: <chain>.No active payout SigningKey configured for chain: ...— payouts are not yet live on this chain.
2. POST /payout/estimate — dry run (no record created)
Checks whether the wallet and the gas station can cover a withdrawal before you make it. Creates nothing; call it as often as needed.
curl -X POST https://api.chainpay.site/payout/estimate \
-H 'content-type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-d '{
"address": "THjTgKDGj3Biar7ZTSmYZAWsqkTKhWGv5Y",
"destination": "TDestinationAddressOfThePayer...",
"amount": "100.5",
"walletAccessToken": "9f2c8a1e4b3d7f60..."
}'
Same fields as /payout/withdraw, without invoiceId.
Response (200)
{
"balance": "250.000000000000000000",
"sufficientBalance": true,
"gasStationAddress": "TDZkhJWMeaFAhefQXK4mG8GtkSZahw13jK",
"gasStationBalance": "12.5",
"sufficientGas": true
}
balance— the wallet's real balance. On TRON it comes from our continuous chain tracking; on other networks it is a live on-chain read, cached for a few minutes so that the next/payout/withdrawcan reject early.sufficientBalance—falseifamountexceeds the balance. This checks the token only; gas is checked separately.gasStationAddress/gasStationBalance— your gas wallet and its native balance.sufficientGas— whethergasStationBalancecovers the estimated network cost of this transfer. Absent (notfalse) if the estimate could not be computed.
3. POST /payout/withdraw — request a withdrawal
Requires x-api-key and this wallet's walletAccessToken. Returns
immediately; the transfer runs asynchronously (background worker, every 15 s).
If a recently known balance of the wallet or gas station clearly does not cover
the amount or network cost, the request is rejected at once (400). This is an
early best-effort check on already known data; the authoritative check happens
right before sending.
curl -X POST https://api.chainpay.site/payout/withdraw \
-H 'content-type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-d '{
"address": "THjTgKDGj3Biar7ZTSmYZAWsqkTKhWGv5Y",
"destination": "TDestinationAddressOfThePayer...",
"amount": "100.5",
"invoiceId": "withdraw-2026-09-14-001",
"walletAccessToken": "9f2c8a1e4b3d7f60..."
}'
| Field | Required | Description |
|---|---|---|
address | yes | Payout wallet address returned by /payout/wallet |
destination | yes | Recipient address (format-checked for the chain) |
amount | yes | Amount in the wallet's token — exactly this reaches destination, no deductions |
invoiceId | yes | Idempotency key. A repeat call with the same invoiceId returns the same record and never double-sends |
walletAccessToken | yes | Token of this wallet (issued once at creation) |
Response (201)
{
"id": "b7e1...",
"status": "pending",
"amount": "100.500000000000000000",
"invoiceId": "withdraw-2026-09-14-001"
}
Errors (400 / 401 / 404)
address, destination, amount, invoiceId and walletAccessToken are required.No payout wallet registered for address: ...(404).Invalid walletAccessToken.destination is not a valid <chain> address: ....amount must be a positive number.Insufficient balance: ...andInsufficient <native currency> on gas station ...— early checks against recently known data; if the data is stale or missing you learn about the shortfall later through afailedwebhook instead.
4. GET /payout/:id/status
A reliable fallback if the webhook did not arrive.
{
"id": "b7e1...",
"status": "confirmed",
"amount": "100.500000000000000000",
"destination": "TDestinationAddressOfThePayer...",
"txHash": "802999e236cf8f...",
"failureReason": null,
"createdAt": "2026-09-14T06:52:48.000Z",
"confirmedAt": "2026-09-14T06:54:49.000Z"
}
status:
pending— accepted, not yet processed or processing in progress;confirmed— funds were sent and confirmed on-chain,txHashis set;failed— the withdrawal did not happen; seefailureReason.
5. Webhooks to callbackUrl
Best-effort, one attempt, no retries (unlike deposit webhooks) —
GET /payout/:id/status is the guaranteed fallback. Like deposit webhooks they
are not HMAC-signed: verify with a secret in the callbackUrl query string.
{
"externalReference": "site-42",
"invoiceId": "withdraw-2026-09-14-001",
"status": "confirmed",
"txHash": "802999e236cf8f..."
}
Sent for both confirmed and failed (then txHash: null; get the reason from
GET /payout/:id/status).
6. Typical integration cycle
- Once per client/site:
POST /payout/walletwith a uniqueexternalReference; storeaddress,walletAccessTokenandgasStationAddress. - Fund
addresswith the token you plan to pay out andgasStationAddresswith the chain's native coin. - Optionally call
POST /payout/estimatebefore a withdrawal. - When a user requests a withdrawal:
POST /payout/withdrawwith a uniqueinvoiceId; you getpendingat once. - Receive the
confirmed/failedwebhook oncallbackUrl. - If no webhook arrives in a reasonable time, fall back to
GET /payout/:id/status.