> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.crisscross.money/testing-first-trade/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.crisscross.money/_mcp/server. # Your First Trade > Step-by-step walkthrough of a first sandbox trade on CrissCross Exchange — token, balance check, quote, acceptance, and confirming the filled order. > **Info** > > **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](/testing-overview#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](/currencies-and-conventions); the steps do not change. > **Note** > > This page is a test procedure. For how quotes and orders behave, see [Trading](/exchange-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](/api-reference/authentication/request-access-token) ```bash 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" }' ``` ```json { "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](/authentication). ### Step 2 — Check your balance `GET /v1/balances` — [API reference](/api-reference/exchange/exchange/balances/get-balances) ```bash curl -G 'https://api.crisscross.money/v1/balances' \ -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ -d 'currency=USD' \ -d 'currency=NGN' ``` ```json { "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](#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](/api-reference/exchange/exchange/rates-and-quotes/request-quote) ```bash 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" }' ``` ```json { "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](/api-reference/exchange/exchange/rates-and-quotes/request-comparative-quotes) and accept the one marked `"executable": true`. ### Step 4 — Accept the quote `POST /v1/quotes/{quoteId}/accept` — [API reference](/api-reference/exchange/exchange/rates-and-quotes/accept-quote) ```bash curl -X POST 'https://api.crisscross.money/v1/quotes/qut_550e8400-e29b-41d4-a716-446655440000/accept' \ -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' ``` ```json { "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](/api-reference/exchange/exchange/balances/get-balances) Repeat the call from Step 2, with the same `currency` filter: ```json { "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](/api-reference/exchange/exchange/balances/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 | | | | -------------- | -------------------------------------------------------------------------------------------- | | Credentials | Your sandbox `client_id` and `client_secret` issue a token that works for Exchange | | Connectivity | Nothing between you and `api.crisscross.money` is blocking the calls | | Request shape | Your quote request is accepted, with amounts as decimal strings | | State handling | You 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: | Scenario | How to produce it | What you should see | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Expired quote | Request a quote, wait until just after `expiresAt`, then accept it | `409`, `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 balance | Quote more than your `available` balance, then accept it | `422`, `INSUFFICIENT_BALANCE`, with `available` and `required` in `error.details` | | Accept replayed | Accept the same `quoteId` twice | The same `orderId` both times — accepting is idempotent on the quote, so a retry never books a second trade | | Non-executable quote | [Request comparative quotes](/api-reference/exchange/exchange/rates-and-quotes/request-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 option | Rejected 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 * [Set up a webhook endpoint](/exchange-webhooks#securing-and-delivery) in your sandbox dashboard, subscribe it to `order.filled`, and re-run this test. See [Testing Webhooks](/core-concepts-webhooks#testing-webhooks) for how to inspect and resend deliveries. * If you move funds out to your own bank account or wallet, run [Your First Withdrawal](/testing-first-withdrawal). > Run one complete currency conversion in the sandbox, end to end