5. Off-ramp: send a payout

Bank payout

POST /v1/payments/withdrawals

curl -X POST https://api-staging.checker.finance/v1/payments/withdrawals \
  -H "X-Api-Key: $API_KEY" \
  -H "X-Timestamp: $TIMESTAMP" \
  -H "X-Nonce: $(uuidgen)" \
  -H "X-Signature: $SIGNATURE" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "acc_01ARZ3NDEKTSV4RRFFQ69G5USD",
    "beneficiary_id": "ben_01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "amount": "100.00",
    "rail": "FEDWIRE",
    "description": "Invoice 4021"
  }'

Response — 202 Accepted:

{
  "withdrawal_id": "wdr_01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "account_id": "acc_01ARZ3NDEKTSV4RRFFQ69G5USD",
  "beneficiary_id": "ben_01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "amount": "100.00",
  "rail": "FEDWIRE",
  "status": "PROCESSING"
}

Use ACH or FEDWIRE for US domestic payouts and SWIFT for a beneficiary outside the US. description is required on every bank rail and is used as the payment reference.

Some SWIFT destination countries (for example Hong Kong, China, India and Kenya) require a purpose_code: PAYMENT_FOR_PHYSICAL_GOODS, PAYMENT_FOR_SERVICES, SALARY_PAYMENT, FAMILY_SUPPORT or OTHER. Not every code is accepted in every country. A payout to such a country without a purpose_code, or with one that country does not accept, returns 202 with status: FAILED and reason_code: INVALID_PURPOSE_CODE before any funds move. Countries that do not require a code ignore it.

The beneficiary is sent exactly amount. The payout fee, reported as fee_amount, is debited from the account in addition to it, and total_amount reports the full debit: the example above sends 100.00 USD and, with a 2.50 fee, debits total_amount 102.50. Both fields appear once the fee is known. Correspondent banks may still deduct their own charges on SWIFT.

On-chain payout

Pay a wallet beneficiary from the settled stablecoin balance. Use the customer's account for the asset being sent as account_id (for a USDT beneficiary, the USDT account — not the USD account the conversion was funded from).

curl -X POST https://api-staging.checker.finance/v1/payments/withdrawals \
  -H "X-Api-Key: $API_KEY" \
  -H "X-Timestamp: $TIMESTAMP" \
  -H "X-Nonce: $(uuidgen)" \
  -H "X-Signature: $SIGNATURE" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "acc_01ARZ3NDEKTSV4RRFFQ69G5USDT",
    "beneficiary_id": "ben_01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "amount": "95.000000",
    "rail": "CRYPTO",
    "description": "On-ramp settlement"
  }'

Notes:

  • CRYPTO is a same-currency on-chain send: the beneficiary's asset must match the account's currency, and amount and the spendable balance are both in that asset. A beneficiary in a different asset returns 400 VALIDATION_ERROR; CRYPTO is not offered on the USD account (400 RAIL_NOT_AVAILABLE).
  • quote_id is not allowed on CRYPTO or on bank rails (400 VALIDATION_ERROR); the conversion, if any, happened earlier.
  • description is optional on CRYPTO and becomes the reference on the transfer.
  • The beneficiary wallet receives exactly amount; the payout fee (fee_amount) is debited in addition to it, and total_amount reports the full debit (amount + fee_amount). To pay out a converted balance, send less than the full balance so the fee fits, as in the example above.
  • Rails outside the account's entitlements are rejected with 400 RAIL_NOT_AVAILABLE; an amount above the spendable balance returns 400 INSUFFICIENT_FUNDS.

Insufficient funds: top up and retry

Before a bank or on-chain payout is submitted, the payout fee is estimated and the request is checked against amount + fee. If the balance cannot cover both, the request is rejected with 400 INSUFFICIENT_FUNDS before any funds move, and no withdrawal is created. details gives the shortfall:

{
  "error_code": "INSUFFICIENT_FUNDS",
  "message": "requested amount plus fee exceeds available balance",
  "details": {
    "fee_amount": "1.55",
    "fee_currency": "USD",
    "required_amount": "6.55",
    "available_amount": "5.00"
  }
}

Top up the account by at least required_amount − available_amount (or lower amount), then retry. The rejected request releases its Idempotency-Key, so the same key can be reused. fee_amount is an estimate; the accepted payout reports the actual fee. If the balance drops between this check and submission, the payout is instead returned with status: FAILED and reason_code: INSUFFICIENT_FUNDS; top up and submit a new payout.

Check the payout status

GET /v1/payments/withdrawals/{withdrawal_id}

curl https://api-staging.checker.finance/v1/payments/withdrawals/wdr_01ARZ3NDEKTSV4RRFFQ69G5FAV \
  -H "X-Api-Key: $API_KEY" \
  -H "X-Timestamp: $TIMESTAMP" \
  -H "X-Nonce: $(uuidgen)" \
  -H "X-Signature: $SIGNATURE"

Response — 200 OK:

{
  "withdrawal_id": "wdr_01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "beneficiary_id": "ben_01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "amount": "100.00",
  "rail": "FEDWIRE",
  "status": "COMPLETED",
  "fee_amount": "2.50",
  "total_amount": "102.50",
  "description": "Invoice 4021"
}

Poll until the withdrawal reaches COMPLETED, FAILED, or CANCELLED, then confirm the balance with GET /v1/payments/accounts/{account_id}/balance. For this payout the balance drops by total_amount (102.50 = amount + fee_amount), and the beneficiary receives 100.00. While the payout is PROCESSING, available already excludes total_amount and total still includes it; total drops once the payout settles. On a quote-driven payout (CHINA_OFFRAMP) total_amount equals amount, because the fee is priced into the quote.



Did this page help you?