Send a payout
Request a withdrawal into your organisation's approval flow, follow it to settlement, and recover it safely after a timeout.
Before you start
- The destination must already be an approved, verified destination — see Destinations.
- Take
wallet_id,assetandnetworkfrom the wallet's balances inGET /api/v1/wallets. - Obtain the payment instruction and persist one idempotency key for this payment before the first attempt — for example your invoice or payout reference.
Request a withdrawal
POST /api/v1/withdrawals submits a withdrawal into your organisation's approval flow. Every payout
passes compliance checks and a human approval before it reaches the network. Neither the API nor the
MCP connector exposes an approval endpoint.
202 means “accepted for approval”, not “sent”.
Never show a user "sent" on a 202 — poll the status endpoint or wait for the webhook.
| Field | Type | Notes |
|---|---|---|
wallet_id | string | Required. From GET /wallets. Up to 128 characters. |
asset | string | Required. Symbol, e.g. ETH. Must be held by that wallet. |
amount | string | Required. Positive decimal STRING, e.g. "0.25", up to 100 characters. More decimals than the asset allows returns 400. |
destination | string | Required. Must already be an approved, verified destination. Up to 128 characters. |
network | string | Use the exact balance network. Required when the asset is held on several networks; if omitted, it resolves to the one network the asset is held on. |
fee_priority | string | Optional. low · medium · high. |
idempotency-key | header | Required. Retry with the SAME value to avoid a second withdrawal. |
Bodies reject unknown fields with 400. Idempotency keys are 1–128 printable ASCII characters without
spaces, and are unique within your organisation. A withdrawal is visible only to keys of the workspace
that created it.
{
"wallet_id": "acct_7f3a…",
"asset": "ETH",
"amount": "0.25",
"destination": "0x03a6…81b9",
"network": "ethereum"
}202 Accepted
{
"data": {
"id": "5f0c2a9e-7d1b-4c3a-9e2f-6b8d4a1c3e57",
"status": "pending_approval",
"wallet_id": "acct_7f3a…",
"asset": "ETH",
"amount": "0.25",
"destination": "0x03a6…81b9",
"network": "ethereum",
"tx_hash": null,
"created_at": "2026-07-30T09:02:11.004Z",
"updated_at": "2026-07-30T09:02:11.004Z"
},
"meta": {}
}With the supplied Node.js client:
// These values come from your persisted payment record.
const paymentKey = "invoice-2026-0042";
const payment = {
wallet_id: "WALLET_ID_FROM_API", asset: "USDC", amount: "25.50",
network: "ethereum", destination: "VERIFIED_DESTINATION_ADDRESS",
fee_priority: "medium"
};
const result = await api.request("POST", "/api/v1/withdrawals", payment, paymentKey);
if (result.status === 202 || result.status === 200) {
const status = await api.request("GET", `/api/v1/withdrawals/${result.body.data.id}`);
console.log(status.body);
}Follow the lifecycle
GET /api/v1/withdrawals/{id} refreshes progress through settlement. Read data.status even on a
successful HTTP response.
received → pending_approval → approved → processing → completed| Status | Meaning |
|---|---|
received | Accepted by us, not yet submitted for approval. |
pending_approval | Pending approval or processing checks. The only state you can cancel from. |
approved | Approved, preparing to send. |
processing | Submitted to the network. |
completed | Settled. tx_hash is populated. |
rejected · failed · expired | Terminal unsuccessful outcome. Investigate before requesting another payment. |
cancelled | Terminal. You cancelled it before approval; nothing was sent. |
If a live refresh fails, the response carries the last recorded state with meta.stale: true. Final
states come from the record. Polling never approves or retries a payment.
Every status change also arrives as a withdrawal.status_changed webhook if
you have one configured. Treat the webhook as a prompt to re-read GET /withdrawals/{id}, which is the
source of truth.
Retry safely
If a POST times out or fails with 500/503 or a network error, retry with the same key and the
same body. The client signs a fresh attempt. You get one of:
200withmeta.idempotent_replay: trueand the original withdrawal — including a recordedfailedor still-receivedstatus. A replay returns recorded state; it never resumes or reissues the payment.202— the earlier attempt never landed, so this one is evaluated afresh.409 idempotency_conflict— that key was already used for a different body. Recover the original payment before proceeding. A collision with a payment outside your workspace also returns a conflict, without exposing that payment.
A request refused before it is accepted — a 400, a 404, a 422 for the wallet or asset, or a
503 — records nothing. Fix the cause if there is one, then retry with the same key: the retry is
evaluated afresh, as if it were the first attempt. Only the same key with a different body is
refused, with 409 idempotency_conflict.
A NEW key on a retry is a NEW withdrawal. Never generate a replacement key for an uncertain payment.
Recover a lost submission
GET /api/v1/withdrawals lists recorded payouts, including a submission whose response was lost.
| Parameter | Type | Notes |
|---|---|---|
limit | integer | 1–100; default 25. |
offset | integer | 0–10000; default 0. Follow meta.next_offset until null. Deduplicate by id when new payments arrive during paging. |
wallet_id | string | Optional wallet filter within this key's workspace. |
idempotency_key | string | Look up the original payment after a timeout. Preserve its key and body when retrying. |
const found = await api.request("GET", `/api/v1/withdrawals?idempotency_key=${encodeURIComponent(paymentKey)}`);Listing returns recorded state. Use GET /withdrawals/{id} to refresh an individual payout. If a payout
stays received or otherwise unresolved, contact your administrator with the withdrawal ID and the
X-Request-Id of your request.
Errors you should handle
| Code | HTTP | Meaning |
|---|---|---|
idempotency_key_required | 400 | POST /withdrawals needs an Idempotency-Key header. |
idempotency_conflict | 409 | That payment key was used for another payload. |
asset_not_held | 422 | That wallet holds no such asset on that network. |
destination_not_whitelisted | 422 | The destination is not an approved, verified destination yet. |
no_approver_available | 422 | The workspace has no second approver configured. |
withdrawal_failed | 422 | The request could not be processed. |
The full list is on Errors.