Skip to navigation

Your First Trade

Run one complete currency conversion in the sandbox, end to end

Relevant if you use Exchange to convert currencies, hold balances or withdraw to your own accounts. If you only collect payments or send payouts, you don’t need this subsection. See Which parts you need.

This is the one Exchange test to run before any other. It converts USD to NGN and checks the filled order in your balances, which proves four things at once: your credentials work for Exchange, you can reach the API, your quote request is accepted, and you can read the result back in your balances.

The example sells USD 1,000.00 for NGN. Substitute any pair from Currencies & Conventions; the steps do not change.

This page is a test procedure. For how quotes and orders behave, see Trading.

Before you start

You’ll need your sandbox client_id and client_secret. Exchange doesn’t use a merchantId.

Your sandbox balances start at zero. Ask your account manager to top up your sandbox organisation in the currency you want to sell — USD for this example.

Step 1 — Get an access token

POST /v1/auth/oauth2/token — API reference

curl -X POST 'https://api.crisscross.money/v1/auth/oauth2/token' \
-H 'Content-Type: application/json' \
--data-raw '{
"client_id": "YOUR_SANDBOX_CLIENT_ID",
"client_secret": "YOUR_SANDBOX_CLIENT_SECRET"
}'
{
"access_token": "eyJhbGci...",
"token_type": "Bearer",
"expires_in": 86400
}

Pass: a 200 with an access_token. Tokens last 24 hours and there is no refresh token — request a new one when it expires.

If this fails: a 401 means the client_id or client_secret is wrong — check you’re using the sandbox set, not production. A 400 usually means the body was form-encoded; it must be JSON. See Authentication.

Step 2 — Check your balance

GET /v1/balances — API reference

curl -G 'https://api.crisscross.money/v1/balances' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-d 'currency=USD' \
-d 'currency=NGN'
{
"balances": [
{ "currency": "USD", "available": "5000.00", "reserved": "0.00", "total": "5000.00" },
{ "currency": "NGN", "available": "0.00", "reserved": "0.00", "total": "0.00" }
],
"nextCursor": null,
"hasMore": false
}

Pass: a 200 with an entry for both USD and NGN, including a zero NGN balance, and at least USD 1,000.00 available. Filtering on currency keeps both entries on the first page — without it the list is paginated, and you’d follow nextCursor to find them.

If USD is 0.00, your sandbox hasn’t been funded yet — see Before you start. Note the balances down; you’ll compare against them in Step 5.

Step 3 — Request a quote

POST /v1/quotes — API reference

curl -X POST 'https://api.crisscross.money/v1/quotes' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
--data-raw '{
"sellCurrency": "USD",
"buyCurrency": "NGN",
"side": "sell",
"amount": "1000.00"
}'
{
"quoteId": "qut_550e8400-e29b-41d4-a716-446655440000",
"status": "active",
"sellCurrency": "USD",
"buyCurrency": "NGN",
"sellAmount": "1000.00",
"buyAmount": "1675000.00",
"rate": "1675.00",
"side": "sell",
"createdAt": "2026-04-21T14:14:55Z",
"expiresAt": "2026-04-21T14:15:30Z"
}

Pass: a 201 with a quoteId, a status of active, and both legs filled in. Keep the quoteId and note the expiresAt — the quote lasts seconds, not minutes, so go straight to the next step.

A few things worth getting right the first time:

  • Amounts are decimal strings, not minor units. "1000.00" is USD 1,000.00. Sending a number rather than a string is a common first-run mistake.
  • side says which leg amount fixes. sell fixes what you give up; buy fixes what you receive. The response fills in the other leg either way.
  • Quoting doesn’t touch your balance. Balances are checked at acceptance, so you can request as many quotes as you like.

If this fails: a 404 with RATE_NOT_AVAILABLE means no rate is available for that pair right now. A 400 means a field is missing or malformed; the error.message names it. If the response tells you to use comparative quotes, your account is enrolled in more than one execution option — request comparative quotes and accept the one marked "executable": true.

Step 4 — Accept the quote

POST /v1/quotes/{quoteId}/accept — API reference

curl -X POST 'https://api.crisscross.money/v1/quotes/qut_550e8400-e29b-41d4-a716-446655440000/accept' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
{
"orderId": "ord_6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"quoteId": "qut_550e8400-e29b-41d4-a716-446655440000",
"status": "filled",
"sellCurrency": "USD",
"buyCurrency": "NGN",
"sellAmount": "1000.00",
"buyAmount": "1675000.00",
"rate": "1675.00",
"createdAt": "2026-04-21T14:15:05Z",
"updatedAt": "2026-04-21T14:15:05Z"
}

Pass: a 201 with an orderId, a status of filled, and sellAmount, buyAmount and rate exactly as quoted. The trade is done and your balances have already moved, so there’s nothing to poll.

If this fails: a 409 with QUOTE_EXPIRED means you were too slow — request a new quote and accept it straight away. A 422 with INSUFFICIENT_BALANCE means your available USD doesn’t cover sellAmount; error.details shows what’s available and what’s required.

Step 5 — Confirm your balances moved

GET /v1/balances — API reference

Repeat the call from Step 2, with the same currency filter:

{
"balances": [
{ "currency": "USD", "available": "4000.00", "reserved": "0.00", "total": "4000.00" },
{ "currency": "NGN", "available": "1675000.00", "reserved": "0.00", "total": "1675000.00" }
],
"nextCursor": null,
"hasMore": false
}

Pass: USD has gone down by sellAmount and NGN has gone up by buyAmount.

For the audit trail, Get balance statement at GET /v1/balances/statements?currency=USD now shows an order_sell entry whose relatedId is your orderId, and the NGN statement shows the matching order_buy. The statement is the source of truth for reconciliation, so it’s worth checking your code can read it now.

What you have just proved

CredentialsYour sandbox client_id and client_secret issue a token that works for Exchange
ConnectivityNothing between you and api.crisscross.money is blocking the calls
Request shapeYour quote request is accepted, with amounts as decimal strings
State handlingYou can accept a quote in time and read the filled order back in your balances and statement

Then try the failures

Exchange has no trigger values in the sandbox. You produce each failure by setting up the state that causes it:

ScenarioHow to produce itWhat you should see
Expired quoteRequest a quote, wait until just after expiresAt, then accept it409, QUOTE_EXPIRED. Expired quotes that were never accepted are eventually deleted, so if you wait too long you’ll get 404, QUOTE_NOT_FOUND instead — handle both
Not enough balanceQuote more than your available balance, then accept it422, INSUFFICIENT_BALANCE, with available and required in error.details
Accept replayedAccept the same quoteId twiceThe same orderId both times — accepting is idempotent on the quote, so a retry never books a second trade
Non-executable quoteRequest comparative quotes and accept one marked "executable": false — an execution option you aren’t enrolled in. Only possible when the platform offers more than one optionRejected with QUOTE_NOT_EXECUTABLE

Branch on error.code, never on error.message. The message is for people and can change.

Only CrissCross can cancel an order, so the sandbox can’t produce a cancelled one. Handle order.cancelled anyway: your balances are reverted when it fires.

Next