> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.crisscross.money/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).