> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.crisscross.money/testing-first-withdrawal/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.crisscross.money/_mcp/server. # Your First Withdrawal > Step-by-step walkthrough of a first sandbox withdrawal on CrissCross Exchange — creating a beneficiary, waiting for it to activate, and withdrawing to it. > **Info** > > **Relevant if you withdraw Exchange balances to your own bank account or wallet.** If CrissCross settles to you on a fixed schedule and you never withdraw through the API, skip this page. Sending money to third parties is a payout, not a withdrawal — test that with [Your First Payout](/testing-first-payout). This test registers a beneficiary, waits for it to become `active`, withdraws to it, and follows the withdrawal to `completed`. It proves the parts of Exchange that trading doesn't: idempotent writes, the beneficiary lifecycle, and how a withdrawal reserves and then debits your balance. The example uses a USDT wallet on Ethereum. If you withdraw in fiat, the steps are the same; see [Fiat beneficiaries](#fiat-beneficiaries) below for the routing details. > **Note** > > This page is a test procedure. For how beneficiaries and withdrawals behave, see [Funding](/exchange-funding#beneficiaries). ### Before you start You need a USDT balance of at least 100.00. Either ask your account manager to top up your sandbox organisation in USDT, or trade into it by following [Your First Trade](/testing-first-trade) with `"buyCurrency": "USDT"`. You'll also need an access token from your sandbox credentials — see Step 1 of [Your First Trade](/testing-first-trade). ### Step 1 — Create a beneficiary `POST /v1/beneficiaries` — [API reference](/api-reference/exchange/exchange/beneficiaries/create-beneficiary) ```bash curl -X POST 'https://api.crisscross.money/v1/beneficiaries' \ -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 3f1c9a52-7d4e-4b8a-9e21-6a0f5c8d2b17' \ --data-raw '{ "name": "Acme Treasury Ltd", "entityType": "business", "address": { "line1": "100 Market Street", "city": "Lagos", "postcode": "100001", "country": "NG" }, "currency": "USDT", "accountDetails": [ { "type": "wallet_address", "value": "YOUR_ETHEREUM_WALLET_ADDRESS" } ], "routingDetails": [ { "rail": "crypto", "network": "ethereum", "isAddressOwner": true, "sendTo": "private_wallet" } ] }' ``` ```json { "beneficiaryId": "ben_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "pending", "currency": "USDT", "name": "Acme Treasury Ltd", "entityType": "business", "accountDetails": [ { "type": "wallet_address", "value": "YOUR_ETHEREUM_WALLET_ADDRESS" } ], "routingDetails": [ { "rail": "crypto", "network": "ethereum" } ], "createdAt": "2026-04-21T14:14:55Z", "activatedAt": null } ``` **Pass:** a `201` with a `beneficiaryId` and a `status` of `pending` — or `active`, if its checks have already passed. Keep the `beneficiaryId`. A few things worth getting right the first time: * **Send an `Idempotency-Key`.** It's optional, but without one a retried request can create a duplicate beneficiary. Generate a UUIDv4, store it before you send, and reuse it on every retry of the same request. * **Crypto beneficiaries need `isAddressOwner` and `sendTo`.** `true` and `private_wallet` mean it's your own self-custodied wallet. Paying someone else, or an account at another exchange, needs more fields — see [Create beneficiary](/api-reference/exchange/exchange/beneficiaries/create-beneficiary). * **One currency per beneficiary.** A wallet you want to receive both USDT and USDC at is two beneficiaries. **If this fails:** a `400` means a field is missing or invalid, often in `routingDetails`. The `error.message` names the field. ### Step 2 — Wait for the beneficiary to activate `GET /v1/beneficiaries/{beneficiaryId}` — [API reference](/api-reference/exchange/exchange/beneficiaries/get-beneficiary) ```bash curl -G 'https://api.crisscross.money/v1/beneficiaries/ben_a1b2c3d4-e5f6-7890-abcd-ef1234567890' \ -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' ``` **Pass:** `status` is `active` and `activatedAt` is set. Poll every few seconds while it's `pending`. If it stays `pending`, ask your account manager to approve it — see [Exchange Sandbox](/exchange-sandbox) for which beneficiaries need approval in the sandbox. In production this step runs real compliance checks and can take longer, or end in `rejected`. Build your code to wait for `active` — by polling or on the `beneficiary.active` webhook — rather than assuming it. ### Step 3 — Create the withdrawal `POST /v1/withdrawals` — [API reference](/api-reference/exchange/exchange/withdrawals/create-withdrawal) ```bash curl -X POST 'https://api.crisscross.money/v1/withdrawals' \ -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 8b2e4f60-1c3a-4d7e-a5b9-0f6d2c8e4a13' \ --data-raw '{ "beneficiaryId": "ben_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "currency": "USDT", "amount": "100.00", "reference": "Sandbox first withdrawal", "clientReference": "SANDBOX-FIRST-WITHDRAWAL-001" }' ``` ```json { "withdrawalId": "wth_6ba7b810-9dad-11d1-80b4-00c04fd430c8", "status": "pending", "statusReason": null, "beneficiaryId": "ben_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "currency": "USDT", "amount": "100.00", "reference": "Sandbox first withdrawal", "clientReference": "SANDBOX-FIRST-WITHDRAWAL-001", "paymentType": "crypto", "createdAt": "2026-04-21T14:20:00.000Z", "updatedAt": "2026-04-21T14:20:00.000Z" } ``` **Pass:** a `201` with a `withdrawalId` and a `status` of `pending`. Check [Get balances](/api-reference/exchange/exchange/balances/get-balances): USDT `available` has dropped by 100.00 and `reserved` has gone up by the same amount. The amount is reserved as soon as the withdrawal is created, so it can't be spent twice. It only becomes a final debit when the withdrawal completes. **If this fails:** `BENEFICIARY_NOT_ACTIVE` means you didn't wait for Step 2. `CURRENCY_MISMATCH` means `currency` doesn't match the beneficiary's. `INSUFFICIENT_BALANCE` means your available USDT doesn't cover `amount` — remember that other pending withdrawals hold part of your balance in `reserved`. ### Step 4 — Poll to a final status `GET /v1/withdrawals/{withdrawalId}` — [API reference](/api-reference/exchange/exchange/withdrawals/get-withdrawal) ```bash curl -G 'https://api.crisscross.money/v1/withdrawals/wth_6ba7b810-9dad-11d1-80b4-00c04fd430c8' \ -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' ``` The sandbox dispatches withdrawals in a batch every 5 minutes, so expect `pending` for up to 5 minutes, then `processing`, then `completed`. Poll every 30 seconds or so. **Pass:** `status` reaches `completed`. Your USDT `reserved` has gone back down by 100.00 and `total` is 100.00 lower than before, and the USDT [balance statement](/api-reference/exchange/exchange/balances/get-balance-statement) shows a `withdrawal` entry with your `withdrawalId` as its `relatedId`. Stop polling at `completed`, `cancelled`, `failed` or `returned`. ### What you have just proved | | | | --------------------- | -------------------------------------------------------------------------- | | Idempotent writes | You send an `Idempotency-Key` on creates, so retries are safe | | Beneficiary lifecycle | You wait for `active` before using a beneficiary | | Balance handling | You read a withdrawal as a reservation first and a debit on completion | | State handling | You can follow a withdrawal through `pending` → `processing` → `completed` | ### Then try the failures As with trading, there are no trigger values. You produce each outcome by setting up the state that causes it: | Scenario | How to produce it | What you should see | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | | Cancel before dispatch | Create a withdrawal and cancel it straight away with [Cancel a withdrawal](/api-reference/exchange/exchange/withdrawals/cancel-withdrawal) — the 5-minute batch gives you the window | `200`, `status: cancelled`, and the reservation released back to `available` | | Cancel replayed | Cancel the same withdrawal again | `200` with the same cancelled withdrawal — cancelling is idempotent | | Cancel too late | Try to cancel once the withdrawal is `processing` or `completed` | `409`, `WITHDRAWAL_NOT_CANCELLABLE` | | Create retried | Resend Step 3 with the same `Idempotency-Key` and body | The original withdrawal back, not a second one | | Beneficiary not ready | Withdraw to a beneficiary that's still `pending` | `BENEFICIARY_NOT_ACTIVE` | | Wrong currency | Withdraw USDC to your USDT beneficiary | `CURRENCY_MISMATCH` | | Not enough balance | Withdraw more than your `available` balance | `422`, `INSUFFICIENT_BALANCE` | The sandbox can't produce a `failed` or `returned` withdrawal, a `rejected` beneficiary, or a `rejected` deposit on demand. Test your handlers for those against the example payloads in [Exchange Webhooks](/exchange-webhooks#payloads), and make sure a `returned` withdrawal restores the balance in your records. ### Fiat beneficiaries In production, a fiat beneficiary is only approved once its compliance documents have been reviewed. The sandbox doesn't ask for documents. Fiat beneficiaries need routing details for their rail. USD takes `international_wire` or `domestic_wire` with a `wireDetails` object, and `swift` is rejected for new beneficiaries. See [Funding](/exchange-funding#beneficiaries). ### Next * Subscribe your [Exchange webhook endpoint](/exchange-webhooks#securing-and-delivery) to the `beneficiary.*` and `withdrawal.*` events, and re-run this test. * When the Exchange tests pass, work through [Going Live](/testing-going-live). > Register a beneficiary and withdraw to it in the sandbox