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

# Testing Rate Locks

> How to test CrissCross rate locks in the sandbox — fixed sandbox corridors, and the drift pair that triggers a rate_lock.cancelled webhook on demand.

[Rate locks](/rate-locks) end in one of three ways: they expire, you cancel them, or **we cancel them because the market moved**. The first two you already know about — you set the expiry and you made the call. Drift cancellation is the one that arrives unannounced, which makes it the one your integration has to handle and the one worth testing deliberately.

The sandbox is built for exactly that. Sandbox corridors are priced from a **fixed mid-market rate** (`midRate`, the rate before your markup), isolated from the live market, so a lock's rate is stable and predictable — except for one corridor that moves on purpose.

### Sandbox corridors

| Pair          | Sandbox `midRate` | Behaviour                        |
| ------------- | ----------------- | -------------------------------- |
| `USD` → `XOF` | 600               | Fixed. Only ever ends by expiry. |
| `USD` → `XAF` | 600               | Fixed. Only ever ends by expiry. |
| `USD` → `NGN` | 1550              | Fixed. Only ever ends by expiry. |
| `USD` → `KES` | \~129, drifting   | **Drift pair** — see below.      |

These are the corridors seeded for sandbox organisations. A pair configured on your account with no entry here has no rate, and creating a lock against it fails with a market-rate error rather than returning a lock.

The mid is fixed, not the all-in rate: your `markupBps` is applied on top as it is in production, so `allInRate` still reflects your own configuration.

### Testing the happy path

Create a lock on any fixed corridor and it behaves exactly as production does, minus the market:

```bash
curl -X POST 'https://api.crisscross.money/v1/rate-locks' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  --data-raw '{ "base": "USD", "quote": "XOF" }'
```

`POST /v1/rate-locks` — [API reference](/api-reference/collect/rate-locks/create-rate-lock)

**Pass:** an `active` lock with `midRate` of `600`, your configured `markupBps`, and an `allInRate` combining the two. Reuse the `rateLockId` across as many checkout sessions as you like; every one prices at `allInRate`.

Because the mid never moves on these corridors, a lock here will not be cancelled out from under you — which is what makes them the right choice for testing everything *other* than drift.

### Testing drift cancellation

`USD` → `KES` is the corridor that moves. Its mid ramps steadily from a base of about **129 up by 3% over 10 minutes**, then resets to the base and repeats.

A lock snapshots the mid at the moment it is created, so from then on the live sandbox mid diverges from the lock's reference at a steady rate. Once that gap crosses your configured drift threshold, the monitor cancels the lock. At the sandbox default the cancellation lands roughly **five minutes** after the lock is minted.

To exercise it:

1. Create a lock on `USD` → `KES`.
2. Wait. Nothing else is required — no market event to arrange, no support request.
3. Your endpoint receives [`rate_lock.cancelled`](/webhook-events) with `cancellationReason: market_drift`.
4. `GET /v1/rate-locks/{rateLockId}` now returns `cancelled`.

**Pass:** you receive the webhook, stop using that `rateLockId`, and fetch a replacement lock. A cancellation is permanent — a lock cancelled by drift stays cancelled even when the sandbox mid resets on its next cycle.

> **Warning**
>
> Do not use `USD` → `KES` for any other rate lock test. A lock on this corridor will be cancelled within minutes whether or not that is what you are testing, which makes it a confusing default for checkout or settlement scenarios. Use a fixed corridor for those.

#### What your integration has to do

When you receive `rate_lock.cancelled`, stop pricing new orders with that lock and create a new one. Payments already in progress still settle at the locked rate, but new checkout sessions that use the cancelled lock are rejected.

The five minutes is approximate, so wait for the webhook in your test rather than a timer.

### What the sandbox does not cover

* **Real rates.** Sandbox rates are round numbers chosen for predictability. They say nothing about the rate you will get in production on the same corridor.
* **Corridors outside the table.** Only the four above are seeded. If you need another for testing, ask your solutions engineer.
* **Your production drift threshold.** It is configured per account, so the roughly-five-minute figure applies to the sandbox default rather than to your live configuration.