#Withdrawals
Pay out from a user's balance
5 endpoints. Generated from docs/openapi.yaml v1.0.0.
| Endpoint | Summary |
|---|---|
GET /v1/withdrawal-fee-quote | Preview a send's exact fees before confirming |
GET /v1/withdrawals | List withdrawals, or look one up by idempotency key |
POST /v1/withdrawals | Request a payout |
GET /v1/withdrawals/{id} | Get a withdrawal |
POST /v1/withdrawals/batch | Batch payouts — up to 50 sends in one call |
#GET/v1/withdrawal-fee-quote
Preview a send's exact fees before confirming · Auth: bearer API key · operationId: withdrawalFeeQuote
Read-only. Our live network cost plus a flat platform fee. network_fee is what this send costs us on-chain, priced now — the energy we rent for the payout and for our fee's own transfer, the bandwidth, and (with user_id) the one-time activation of a sender address that has never held TRX — converted at the live TRX price with a cushion and rounded up to the cent. platform_fee is your account's flat schedule from GET /v1/fees (the platform's own figure is public on GET /v1/pricing); total_fee is their sum and is what POST /v1/withdrawals holds on top of amount. Show the user total_fee; the figure returned here is the one charged.
Pass to_address so the recipient is priced exactly (an address that holds none of the token costs about twice one that does — recipient_priced says it was probed) and user_id so a first send's activation is included. network_cost says what the figure was built from.
dynamic: false means the live TRX price could not be read and the asset's static fallback (static_fallback on GET /v1/pricing) was charged instead. When only the energy market is unreadable the energy part is priced at a fixed fallback rate and the quote stays dynamic: true.
#Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
asset_id | query | string | yes | — |
amount | query | string | no | Optional decimal amount; only affects percentage-based platform fees. |
to_address | query | string | no | Optional recipient. Validated for the asset's chain (400 invalid_address otherwise) and priced exactly: an address holding none of the token costs about twice one that does. Without it the worst case is quoted. |
user_id | query | string | no | Optional sender (your user id). The send leaves this user's own address; if it has never held TRX its one-time activation (about 2.5 TRX) is part of the quote. |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Itemized fee preview. |
401 | Error | Missing, malformed or unknown API key. |
200 response body
| Field | Type | Description |
|---|---|---|
asset_id | string | — |
network_fee | string | A monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string. |
network_fee_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
platform_fee | string | A monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string. |
platform_fee_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
merchant_fee | string | Your own fee on this send (Profit → Your fees); 0 unless you set one, and 0 for your own balance. |
merchant_fee_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
total_fee | string | A monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string. |
total_fee_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
dynamic | boolean | true when the network fee was priced live (the normal case); false means the static fallback was charged. |
quoted_at | string<date-time> | When this quote was priced. Every figure here is computed for this response, never stored. |
recipient_priced | boolean | true when to_address was given and probed, so the figure is exact for that recipient. |
network_cost | object | What the live network figure was built from; null on the static fallback. |
network_cost.energy_trx required | string | TRX for the energy the transfers use (rented |
network_cost.bandwidth_trx required | string | TRX for the bandwidth the transfers burn. |
network_cost.activation_trx required | string | TRX to create the sender's account on its first send; "0" otherwise. |
network_cost.trx_usd required | number | The TRX price the bill was converted at. |
network_cost.transfers required | integer | On-chain transfers in the operation (a payout or load and its fee leg = 2). |
network_cost.recipient required | string | Whether the recipient was priced as holding the token already. (one of fresh, existing, unknown) |
network_cost.first_send required | boolean | Whether the sender's activation was included. |
{
"asset_id": "string",
"network_fee": "1.5",
"network_fee_base": "1500000000",
"platform_fee": "1.5",
"platform_fee_base": "1500000000",
"merchant_fee": "1.5",
"merchant_fee_base": "1500000000",
"total_fee": "1.5",
"total_fee_base": "1500000000",
"dynamic": true,
"quoted_at": "2026-08-31T09:15:00.000Z",
"recipient_priced": true,
"network_cost": {
"energy_trx": "string",
"bandwidth_trx": "string",
"activation_trx": "string",
"trx_usd": 0,
"transfers": 1,
"recipient": "fresh",
"first_send": true
}
}#Example request
curl -X GET 'https://api.cryptoinfra.build/v1/withdrawal-fee-quote?asset_id=string' \
-H 'Authorization: Bearer ck_live_your_key'#GET/v1/withdrawals
List withdrawals, or look one up by idempotency key · Auth: bearer API key · operationId: listWithdrawals
Newest-first, scoped to your merchant. The idempotency_key lookup is the resolution path for an INDETERMINATE create (timeout, dropped connection): it tells you whether the payout landed, so you never have to re-send one to find out. Returns 404 when that key has no withdrawal.
Every filter is additive; with none supplied the list is the one it has always been. total is counted through the SAME filter as the rows, so page with offset until you have total rows. from/to accept either a bare IST calendar day (YYYY-MM-DD, both ends included) or a full RFC 3339 instant with an offset, and filter on when the payout was requested.
#Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
idempotency_key | query | string | no | Exact-match lookup. withdrawals is unique on (tenant, key). |
from | query | string | no | Only withdrawals requested on/after this IST day (or instant). |
to | query | string | no | Only withdrawals requested on/before this IST day (or before this instant). |
status | query | string | no | Exact status match. (one of requested, queued, awaiting_approval, signing, broadcast, confirming, confirmed, failed, reversed) |
search | query | string | no | Case-insensitive substring match on the destination address or the payer's external_user_id. |
limit | query | integer | no | — (default 50; min 1; max 200) |
offset | query | integer | no | — (default 0; min 0) |
sort | query | string | no | — (one of id, created_at, amount, status, chain, fee; default "id") |
dir | query | string | no | — (one of asc, desc; default "desc") |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Matching withdrawals, plus the total matching the same filter. |
401 | Error | Missing, malformed or unknown API key. |
404 | — | No withdrawal for that idempotency_key. |
200 response body
| Field | Type | Description |
|---|---|---|
total required | integer | Withdrawals matching the filter, ignoring limit/offset. |
limit required | integer | — |
offset required | integer | — |
withdrawals required | object[] | — |
withdrawals[].id required | integer | — |
withdrawals[].status required | string | Lifecycle. awaiting_approval = parked by your own maker-checker threshold (set from the dashboard under Settings → Security): the hold is placed and nothing is signed until an owner or finance member approves it from the dashboard's Payouts page (→ queued); a rejection refunds the hold and arrives as withdrawal.failed (→ failed). failed = never broadcast (hold refunded); reversed = broadcast then definitively failed on-chain (hold refunded, operator reconciliation). (one of requested, queued, awaiting_approval, signing, broadcast, confirming, confirmed, failed, reversed) |
withdrawals[].chain required | string | The chain an address or asset is on. TRON is the only network the platform serves: btc, eth, bsc and sol were retired — historical addresses on them are never handed out again and nothing scans them. Read the value as an open string rather than a closed list, so a chain added later does not break your client. (one of tron) |
withdrawals[].asset_id required | string | — |
withdrawals[].to_address required | string | — |
withdrawals[].amount required | string | A monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string. |
withdrawals[].amount_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
withdrawals[].fee required | string | The network fee, gas included, charged on top in the sent asset. |
withdrawals[].fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
withdrawals[].fee_asset_id required | string | The asset fee is charged in. Equals asset_id on every payout since 2026-09-05. A payout created before that date may say trx: an option, since retired, let the network fee be paid from the customer's TRX balance, and such a fee is reported in the coin it was actually paid in. |
withdrawals[].platform_fee required | string | The flat platform fee from your schedule (GET /v1/fees; the platform's own figure is public on GET /v1/pricing). fee is the live network cost of the send. Total held = amount + fee + platform_fee. |
withdrawals[].platform_fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
withdrawals[].txid | string | null | — |
withdrawals[].confirmations required | integer | — |
withdrawals[].error | string | null | — |
withdrawals[].idempotency_key required | string | — |
withdrawals[].created_at | string<date-time> | Present on GET. |
withdrawals[].updated_at | string<date-time> | Present on GET. |
{
"total": 1,
"limit": 1,
"offset": 1,
"withdrawals": [
{
"id": 1,
"status": "requested",
"chain": "tron",
"asset_id": "string",
"to_address": "string",
"amount": "1.5",
"amount_base": "1500000000",
"fee": "1.5",
"fee_base": "1500000000",
"fee_asset_id": "string",
"platform_fee": "1.5",
"platform_fee_base": "1500000000",
"txid": "string",
"confirmations": 1,
"error": "string",
"idempotency_key": "string",
"created_at": "2026-08-31T09:15:00.000Z",
"updated_at": "2026-08-31T09:15:00.000Z"
}
]
}#Example request
curl -X GET 'https://api.cryptoinfra.build/v1/withdrawals?from=2026-08-01&to=2026-08-31' \
-H 'Authorization: Bearer ck_live_your_key'#POST/v1/withdrawals
Request a payout · Auth: bearer API key · operationId: createWithdrawal
Holds amount + fee + platform_fee from the user's balance and queues the payout. amount is what the recipient receives. Idempotent on idempotency_key. Poll GET /v1/withdrawals/{id} or consume the withdrawal.confirmed / withdrawal.failed webhooks.
Approval thresholds. If you have set an approval threshold for this asset (dashboard → Settings → Security; owner or finance login), an API-key send at or above it answers 202 with status: awaiting_approval: the hold is placed, nothing is signed, and it is not queued until an owner or finance member approves it from the dashboard (→ queued). A rejection refunds the hold and arrives as withdrawal.failed with error: "rejected from the dashboard".
#Request body
application/json · required · schema CreateWithdrawal
| Field | Type | Description |
|---|---|---|
external_user_id required | string | (minLength 1; maxLength 255) |
asset_id required | string | An id from GET /v1/assets. |
to_address required | string | Destination address (validated for the asset's chain). |
amount required | string | Decimal amount the recipient receives (fees are charged on top). (pattern ^\d+(\.\d+)?$) |
idempotency_key required | string | (minLength 1; maxLength 255) |
fee_mode | string | Deprecated and ignored. Until 2026-09-05 this chose which balance paid the network fee (auto / asset / trx); every network fee is now charged in the sent asset. The field is still accepted, with any value, so an older client keeps working — it changes nothing. |
#Responses
| Status | Body | Meaning |
|---|---|---|
202 | Withdrawal | Payout accepted — queued, or awaiting_approval when your own threshold parked it for a human. |
400 | Error | Refused before anything was held. error is one of: validationerror, unknownasset, assetdisabled, invalidamount, amountbelowminimum (a zero or negative amount — a payout has no other minimum), invalidaddress, selftransfer (the recipient is the user's own deposit address — nothing would move but the network fee), invalid_request. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown externaluserid for this tenant. |
409 | object | Two different conditions share this status, and they need different handling — branch on error, never on the code alone: - insufficient_balance — the user cannot cover amount + fee + platform_fee. Do not retry. - idempotency_conflict — this idempotency_key was already used with DIFFERENT parameters. Your replay is not the request you think it is. |
503 | Error | No chain adapter configured for the asset's chain. |
202 response body — Withdrawal
| Field | Type | Description |
|---|---|---|
id required | integer | — |
status required | string | Lifecycle. awaiting_approval = parked by your own maker-checker threshold (set from the dashboard under Settings → Security): the hold is placed and nothing is signed until an owner or finance member approves it from the dashboard's Payouts page (→ queued); a rejection refunds the hold and arrives as withdrawal.failed (→ failed). failed = never broadcast (hold refunded); reversed = broadcast then definitively failed on-chain (hold refunded, operator reconciliation). (one of requested, queued, awaiting_approval, signing, broadcast, confirming, confirmed, failed, reversed) |
chain required | string | The chain an address or asset is on. TRON is the only network the platform serves: btc, eth, bsc and sol were retired — historical addresses on them are never handed out again and nothing scans them. Read the value as an open string rather than a closed list, so a chain added later does not break your client. (one of tron) |
asset_id required | string | — |
to_address required | string | — |
amount required | string | A monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string. |
amount_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
fee required | string | The network fee, gas included, charged on top in the sent asset. |
fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
fee_asset_id required | string | The asset fee is charged in. Equals asset_id on every payout since 2026-09-05. A payout created before that date may say trx: an option, since retired, let the network fee be paid from the customer's TRX balance, and such a fee is reported in the coin it was actually paid in. |
platform_fee required | string | The flat platform fee from your schedule (GET /v1/fees; the platform's own figure is public on GET /v1/pricing). fee is the live network cost of the send. Total held = amount + fee + platform_fee. |
platform_fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
txid | string | null | — |
confirmations required | integer | — |
error | string | null | — |
idempotency_key required | string | — |
created_at | string<date-time> | Present on GET. |
updated_at | string<date-time> | Present on GET. |
{
"id": 1,
"status": "requested",
"chain": "tron",
"asset_id": "string",
"to_address": "string",
"amount": "1.5",
"amount_base": "1500000000",
"fee": "1.5",
"fee_base": "1500000000",
"fee_asset_id": "string",
"platform_fee": "1.5",
"platform_fee_base": "1500000000",
"txid": "string",
"confirmations": 1,
"error": "string",
"idempotency_key": "string",
"created_at": "2026-08-31T09:15:00.000Z",
"updated_at": "2026-08-31T09:15:00.000Z"
}#Example request
curl -X POST 'https://api.cryptoinfra.build/v1/withdrawals' \
-H 'Authorization: Bearer ck_live_your_key' \
-H 'Content-Type: application/json' \
-d '{
"external_user_id": "string",
"asset_id": "string",
"to_address": "string",
"amount": "25.5",
"idempotency_key": "string"
}'#GET/v1/withdrawals/{id}
Get a withdrawal · Auth: bearer API key · operationId: getWithdrawal
#Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | — (min 1) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | Withdrawal | The withdrawal (includes timestamps). |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Record not found (or belongs to another tenant). |
200 response body — Withdrawal
| Field | Type | Description |
|---|---|---|
id required | integer | — |
status required | string | Lifecycle. awaiting_approval = parked by your own maker-checker threshold (set from the dashboard under Settings → Security): the hold is placed and nothing is signed until an owner or finance member approves it from the dashboard's Payouts page (→ queued); a rejection refunds the hold and arrives as withdrawal.failed (→ failed). failed = never broadcast (hold refunded); reversed = broadcast then definitively failed on-chain (hold refunded, operator reconciliation). (one of requested, queued, awaiting_approval, signing, broadcast, confirming, confirmed, failed, reversed) |
chain required | string | The chain an address or asset is on. TRON is the only network the platform serves: btc, eth, bsc and sol were retired — historical addresses on them are never handed out again and nothing scans them. Read the value as an open string rather than a closed list, so a chain added later does not break your client. (one of tron) |
asset_id required | string | — |
to_address required | string | — |
amount required | string | A monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string. |
amount_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
fee required | string | The network fee, gas included, charged on top in the sent asset. |
fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
fee_asset_id required | string | The asset fee is charged in. Equals asset_id on every payout since 2026-09-05. A payout created before that date may say trx: an option, since retired, let the network fee be paid from the customer's TRX balance, and such a fee is reported in the coin it was actually paid in. |
platform_fee required | string | The flat platform fee from your schedule (GET /v1/fees; the platform's own figure is public on GET /v1/pricing). fee is the live network cost of the send. Total held = amount + fee + platform_fee. |
platform_fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
txid | string | null | — |
confirmations required | integer | — |
error | string | null | — |
idempotency_key required | string | — |
created_at | string<date-time> | Present on GET. |
updated_at | string<date-time> | Present on GET. |
{
"id": 1,
"status": "requested",
"chain": "tron",
"asset_id": "string",
"to_address": "string",
"amount": "1.5",
"amount_base": "1500000000",
"fee": "1.5",
"fee_base": "1500000000",
"fee_asset_id": "string",
"platform_fee": "1.5",
"platform_fee_base": "1500000000",
"txid": "string",
"confirmations": 1,
"error": "string",
"idempotency_key": "string",
"created_at": "2026-08-31T09:15:00.000Z",
"updated_at": "2026-08-31T09:15:00.000Z"
}#Example request
curl -X GET 'https://api.cryptoinfra.build/v1/withdrawals/1' \
-H 'Authorization: Bearer ck_live_your_key'#POST/v1/withdrawals/batch
Batch payouts — up to 50 sends in one call · Auth: bearer API key
Each item runs through the same path as a single withdrawal (fees and approval thresholds apply per item) and carries its own idempotency key, so re-POSTing a batch after a timeout re-creates nothing. Items are independent — one bad address never sinks the rest. An infrastructure failure stops the march mid-batch; retry the whole batch, the keys de-duplicate.
#Request body
application/json · required
| Field | Type | Description |
|---|---|---|
items required | object[] | — |
items[].external_user_id required | string | (minLength 1; maxLength 255) |
items[].asset_id required | string | An id from GET /v1/assets. |
items[].to_address required | string | Destination address (validated for the asset's chain). |
items[].amount required | string | Decimal amount the recipient receives (fees are charged on top). (pattern ^\d+(\.\d+)?$) |
items[].idempotency_key required | string | (minLength 1; maxLength 255) |
items[].fee_mode | string | Deprecated and ignored. Until 2026-09-05 this chose which balance paid the network fee (auto / asset / trx); every network fee is now charged in the sent asset. The field is still accepted, with any value, so an older client keeps working — it changes nothing. |
#Responses
| Status | Body | Meaning |
|---|---|---|
202 | object | Per-item outcomes, in order. |
202 response body
| Field | Type | Description |
|---|---|---|
total | integer | — |
accepted | integer | — |
failed | integer | — |
results | object[] | — |
results[].idempotency_key | string | — |
results[].ok | boolean | — |
results[].withdrawal | object | — |
results[].withdrawal.id required | integer | — |
results[].withdrawal.status required | string | Lifecycle. awaiting_approval = parked by your own maker-checker threshold (set from the dashboard under Settings → Security): the hold is placed and nothing is signed until an owner or finance member approves it from the dashboard's Payouts page (→ queued); a rejection refunds the hold and arrives as withdrawal.failed (→ failed). failed = never broadcast (hold refunded); reversed = broadcast then definitively failed on-chain (hold refunded, operator reconciliation). (one of requested, queued, awaiting_approval, signing, broadcast, confirming, confirmed, failed, reversed) |
results[].withdrawal.chain required | string | The chain an address or asset is on. TRON is the only network the platform serves: btc, eth, bsc and sol were retired — historical addresses on them are never handed out again and nothing scans them. Read the value as an open string rather than a closed list, so a chain added later does not break your client. (one of tron) |
results[].withdrawal.asset_id required | string | — |
results[].withdrawal.to_address required | string | — |
results[].withdrawal.amount required | string | A monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string. |
results[].withdrawal.amount_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
results[].withdrawal.fee required | string | The network fee, gas included, charged on top in the sent asset. |
results[].withdrawal.fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
results[].withdrawal.fee_asset_id required | string | The asset fee is charged in. Equals asset_id on every payout since 2026-09-05. A payout created before that date may say trx: an option, since retired, let the network fee be paid from the customer's TRX balance, and such a fee is reported in the coin it was actually paid in. |
results[].withdrawal.platform_fee required | string | The flat platform fee from your schedule (GET /v1/fees; the platform's own figure is public on GET /v1/pricing). fee is the live network cost of the send. Total held = amount + fee + platform_fee. |
results[].withdrawal.platform_fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
results[].withdrawal.txid | string | null | — |
results[].withdrawal.confirmations required | integer | — |
results[].withdrawal.error | string | null | — |
results[].withdrawal.idempotency_key required | string | — |
results[].withdrawal.created_at | string<date-time> | Present on GET. |
results[].withdrawal.updated_at | string<date-time> | Present on GET. |
results[].error | object | — |
results[].error.code | string | — |
results[].error.message | string | — |
{
"total": 1,
"accepted": 1,
"failed": 1,
"results": [
{
"idempotency_key": "string",
"ok": true,
"withdrawal": {
"id": 1,
"status": "requested",
"chain": "tron",
"asset_id": "string",
"to_address": "string",
"amount": "1.5",
"amount_base": "1500000000",
"fee": "1.5",
"fee_base": "1500000000",
"fee_asset_id": "string",
"platform_fee": "1.5",
"platform_fee_base": "1500000000",
"txid": "string",
"confirmations": 1,
"error": "string",
"idempotency_key": "string",
"created_at": "2026-08-31T09:15:00.000Z",
"updated_at": "2026-08-31T09:15:00.000Z"
},
"error": {
"code": "string",
"message": "string"
}
}
]
}#Example request
curl -X POST 'https://api.cryptoinfra.build/v1/withdrawals/batch' \
-H 'Authorization: Bearer ck_live_your_key' \
-H 'Content-Type: application/json' \
-d '{
"items": [
{
"external_user_id": "string",
"asset_id": "string",
"to_address": "string",
"amount": "25.5",
"idempotency_key": "string"
}
]
}'