Sending payouts
Payouts in depth — destinations, mandatory idempotency, approval flows, and reconciling the result.
A payout is money out to your end user. OmiCloud creates the transaction and submits it to the gateway, which sends the funds from its own account to the destination you supply. There is no hosted page; the whole flow is server to server.
your app ──POST /v1/payouts──▶ OmiCloud ──▶ gateway ──sends──▶ end user's address
│
└──callback──▶ OmiCloud ──webhook──▶ you
Before you call
- Approve first, then call. OmiCloud does not hold or review payouts. By the time you call
POST /v1/payoutsyou should have done your own checks (balance, limits, fraud, KYC). The gateway may apply its own review on top — that shows asgatewayState: PENDING_APPROVALwhilestatestaysprocessing. - Check the route.
GET /v1/routes?direction=payout¤cy=USDTgives you limits and which networks are available for sending.
The request
curl -X POST https://api.omicloud.io/v1/payouts \
-H "Authorization: Bearer omi_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: wd-20260902-4413" \
-d '{
"amount": "250",
"currency": "USDT",
"network": "TRX",
"destination": { "type": "crypto_address", "address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE" },
"endUserId": "player-8812",
"operatorReference": "wd-20260902-4413",
"description": "Withdrawal #4413"
}'Idempotency-Key is required. A payout without one is rejected with 400 VALIDATION_FAILED — the risk of paying twice on a retry is too high to make it optional. Use your own withdrawal id. See Idempotency.
Destinations
destination is a typed object so new rails can be added without changing existing callers:
type | Fields | Available |
|---|---|---|
crypto_address | address, optional tag (memo / destination tag where the chain needs one) | now |
bank_account | — | planned; will be announced in the changelog |
OmiCloud validates the address format for the network and returns 400 VALIDATION_FAILED on a mismatch — for instance an 0x… address with network: TRX. It cannot tell whether the address is the right one; that is between you and your end user.
The response
{ "transaction": "txn_2b7d9e4f11a0", "state": "processing", "gateway": "dce", "gatewayReference": "wd_7f1c2a" }201 means the gateway accepted the payout, not that it has been sent. Debit the end user's balance now (you already reserved it during approval); release the reservation if it later declines.
Tracking
The payout settles when the gateway confirms the transfer — for crypto, when the transaction has enough confirmations. You receive transaction.settled, and GET /v1/transactions/{reference} then has crypto.txHash. Show the hash to the end user; it is their proof.
If it fails you receive transaction.declined. Typical declineReason values for payouts:
declineReason | What happened | What to do |
|---|---|---|
gateway_rejected | The gateway refused it; gatewayState and gatewayDetails in the console say why. | Investigate; usually a compliance hold or a rejected address. |
invalid_destination | The gateway could not send to this address. | Ask the end user to check it. |
insufficient_funds | The gateway account could not cover it. OmiCloud is alerted. | Retry later with a new idempotency key. |
gateway_unreachable | OmiCloud could not reach the gateway when submitting. | Safe to retry with a new key. |
A declined payout never sent money. Return the reserved balance to the end user.
Timing expectations
| Network | Typical time to settled |
|---|---|
| TRX | 1–3 minutes |
| BNB | 1–3 minutes |
| ETH | 3–10 minutes, longer under congestion |
| SOL | under 1 minute |
A payout still processing after an hour is unusual. Check gatewayState — PENDING_APPROVAL means the gateway is reviewing it manually. OmiCloud's own monitoring flags stuck payouts; you do not need to escalate unless the end user is waiting.
Reconciling
Once a day, list settled payouts and compare against your ledger:
GET /v1/transactions?direction=payout&state=settled&since=<yesterday>&limit=100
Match on operatorReference. Any settled payout you do not have a record of is worth a support ticket with its reference.
Updated about 2 months ago