Skip to navigation

Your First Withdrawal

Register a beneficiary and withdraw to it in the sandbox

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.

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 below for the routing details.

This page is a test procedure. For how beneficiaries and withdrawals behave, see Funding.

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 with "buyCurrency": "USDT".

You’ll also need an access token from your sandbox credentials — see Step 1 of Your First Trade.

Step 1 — Create a beneficiary

POST /v1/beneficiaries — API reference

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"
}
]
}'
{
"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.
  • 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

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 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

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"
}'
{
"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: 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

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 shows a withdrawal entry with your withdrawalId as its relatedId.

Stop polling at completed, cancelled, failed or returned.

What you have just proved

Idempotent writesYou send an Idempotency-Key on creates, so retries are safe
Beneficiary lifecycleYou wait for active before using a beneficiary
Balance handlingYou read a withdrawal as a reservation first and a debit on completion
State handlingYou 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:

ScenarioHow to produce itWhat you should see
Cancel before dispatchCreate a withdrawal and cancel it straight away with Cancel a withdrawal — the 5-minute batch gives you the window200, status: cancelled, and the reservation released back to available
Cancel replayedCancel the same withdrawal again200 with the same cancelled withdrawal — cancelling is idempotent
Cancel too lateTry to cancel once the withdrawal is processing or completed409, WITHDRAWAL_NOT_CANCELLABLE
Create retriedResend Step 3 with the same Idempotency-Key and bodyThe original withdrawal back, not a second one
Beneficiary not readyWithdraw to a beneficiary that’s still pendingBENEFICIARY_NOT_ACTIVE
Wrong currencyWithdraw USDC to your USDT beneficiaryCURRENCY_MISMATCH
Not enough balanceWithdraw more than your available balance422, 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, 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.

Next