CChainPay
Accedi

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"
  }'
FieldRequiredDescription
chainyestron, bsc, eth, polygon or arbitrum
tokenSymbolyesToken you will withdraw from this wallet (e.g. USDT)
externalReferenceyesYour own id (site / client / account)
callbackUrlyesPublic 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/withdraw can reject early.
  • sufficientBalance — false if amount exceeds the balance. This checks the token only; gas is checked separately.
  • gasStationAddress / gasStationBalance — your gas wallet and its native balance.
  • sufficientGas — whether gasStationBalance covers the estimated network cost of this transfer. Absent (not false) 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..."
  }'
FieldRequiredDescription
addressyesPayout wallet address returned by /payout/wallet
destinationyesRecipient address (format-checked for the chain)
amountyesAmount in the wallet's token — exactly this reaches destination, no deductions
invoiceIdyesIdempotency key. A repeat call with the same invoiceId returns the same record and never double-sends
walletAccessTokenyesToken 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: ... and Insufficient <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 a failed webhook 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, txHash is set;
  • failed — the withdrawal did not happen; see failureReason.

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

  1. Once per client/site: POST /payout/wallet with a unique externalReference; store address, walletAccessToken and gasStationAddress.
  2. Fund address with the token you plan to pay out and gasStationAddress with the chain's native coin.
  3. Optionally call POST /payout/estimate before a withdrawal.
  4. When a user requests a withdrawal: POST /payout/withdraw with a unique invoiceId; you get pending at once.
  5. Receive the confirmed / failed webhook on callbackUrl.
  6. If no webhook arrives in a reasonable time, fall back to GET /payout/:id/status.