Reference
Sandbox
How to exercise the whole integration against real prices and simulated fills: in the production pipeline, as a flagged tenant rather than a mock.
What the sandbox is
The sandbox is the production pipeline with the execution layer simulated. Live prices, real quote expiry, real idempotency, real settlement: and a fill produced locally instead of at a venue. That is deliberate: a sandbox with its own pricing and its own order path would stop being evidence that the real path works.
| Behaviour | Sandbox |
|---|---|
| Prices | Live upstream books, with real age and real freshness gating. |
| Quote expiry | Real. 30 seconds, enforced server-side. |
| Fill | Simulated, at the accepted price, instantly. Rejected if the book moved past it. |
| Partial fills | Not produced spontaneously, because the simulated venue has no book to run out of. Arm one with POST /api/v1/sandbox/scenarios, below. |
| Positions | Real rows, real weighted averages. |
| Settlement | Real, when the upstream contract resolves. Cash movement is yours. |
| Webhook delivery | Built, and off unless a deployment enabled it. Poll the settlement feed. |
Test users
There is no user provisioning call. A player exists the first time you name one: send any opaque id on X-Parity-User and it is created and mapped on that first request.
# Two players, no setup step
curl ... -H "X-Parity-User: test-alice" ...
curl ... -H "X-Parity-User: test-bob" ...- Use a recognisable prefix so test identities are separable from real ones later.
- Ids are opaque to Parity and are the entire record. No name, no email, nothing else is stored, which is what keeps Parity out of scope for your data obligations.
- Positions and settlements are scoped to the pair of (operator, player), so two test users never see each other.
Test funds, and what they are for
A sandbox tenant does have simulated money. Test funds arrive as a SANDBOX_CREDIT ledger movement from house equity, a real double-entry posting rather than a number assigned to a field, and GET /api/v1/users/{id}/balance and GET /api/v1/users/{id}/ledger read it back.
They exist to be learned against, not integrated against
Those two endpoints, all three funding routes and the two test controls below answer 403 sandbox_only for a live operator. In production you own your customers' cash ledger and Parity returns the exact amounts to apply against it: operatorMoney, operatorExitMoney, settlementCredit.
So the useful way to use the sandbox is to run your own wallet beside it: reserve authorizationAmount on your side, apply actualDebit and releaseAmount, and compare your books against GET /api/v1/reconciliation. That is the part that will actually be moving cash, and it is the only part the sandbox cannot do for you.
Everything downstream (reserve, buy, fee, settlement, void refund) posts exactly as it would live, which is what makes a reconciliation written against the sandbox the one you run in production.
Exercising the failure modes
The paths worth testing are the refusals, not the happy path. Each of these is reachable in the sandbox.
| To produce | Do this |
|---|---|
409 quote_expired | Take a quote, wait more than 30 seconds, submit it. |
409 quote_already_used | Submit one quote twice with two different clientOrderIds. The second refusal carries orderId, naming the order that spent it. That is the only recovery: your own clientOrderId finds nothing. |
200 idempotent replay | Submit the same clientOrderId twice. The second returns the first order. |
400 invalid_request | Send side: "MAYBE". |
400 missing_user | Omit the X-Parity-User header. |
401 invalid_key | Send a bearer token that is not a key. |
404 market_not_found | Quote an outcome id that does not exist. |
422 stake_exceeds_stale_limit | Quote a large stake on a thinly-traded contract whose price has aged into the stale band. |
The sandbox is a tenant, not a mock
Sandbox is a property of the operator, held in a mode column, not a flag on a request. No header a caller invents can move them across the boundary, and its trades are real rows written by the real code.
- A sandbox operator's keys are minted
pk_test_; a live operator's aresk_live_. The spaces do not overlap and the mode decides which you get. - The mode is what gates the balance, ledger, funding and test-control routes. They answer
403 sandbox_onlyfor a live tenant, and every other path behaves identically in both. - One thing the sandbox cannot show you is what happens after an order is cleared to execute. Here the fill is produced immediately. Live, an order is transmitted to a venue only when venue execution is enabled for your deployment and that order has been armed, so until then it rests at
pending_venue_executionwithtransmissionState: not_transmitted. Nothing is lost there and nothing was sent, but it is not a state that clears on its own. Confirm what is armed for your deployment as part of going live.
Testing failure and settlement paths
Two paths decide whether your integration moves money correctly, and until now neither could be rehearsed. A partial fill is the only case where the amount debited is not the amount you authorized. A settlement is the only case where money moves toward your customer. Both depend on a venue and a clock you do not control, so integrations have historically gone live having never run them.
Two sandbox-only endpoints exist so you can. They answer 403 sandbox_only for a live tenant, and they drive the same order and settlement code your production traffic will, rather than a simulation of it. If they produced figures a different way, certifying against them would certify the other way.
A partial fill, on demand
Arm a scenario, then place an order exactly as you normally would.
# Consume $9.87 of the next order, whatever it authorizes
curl -X POST $PARITY_BASE/api/v1/sandbox/scenarios \
-H "Authorization: Bearer $PARITY_KEY" \
-H "X-Parity-User: your-customer-id" \
-H "Content-Type: application/json" \
-d '{"kind":"partial_fill","fillAmountMinor":987}'
# Then quote and submit a $25.00 order as usual. It comes back:
# "status": "partially_filled"
# "operatorMoney": {
# "authorizationAmount": "25.00",
# "actualDebit": "9.87",
# "releaseAmount": "15.13"
# }authorizationAmount = actualDebit + releaseAmount, exactly, in whole minor units. Book the debit, hand back the release. Give fillFraction instead of fillAmountMinor if a proportion suits your test better; give clientOrderId to arm one specific order rather than the next one, which is what you want if your suite runs in parallel. A scenario fires once and expires after an hour.
A win, a loss, or a void
Resolve one of your own open positions and the ordinary settlement runs against it.
curl -X POST $PARITY_BASE/api/v1/sandbox/resolutions \
-H "Authorization: Bearer $PARITY_KEY" \
-H "Content-Type: application/json" \
-d '{"positionId":"pos_...","resolution":"WIN"}'
# A settlement record appears on /api/v1/settlements carrying an exact
# settlementCredit (or voidCredit for a VOID). Book that figure.WIN and LOSS are relative to the position's own side, so you never have to reason about a NO holder winning on a NO resolution. A VOID refunds according to the same money contract a real void would.
Retrying, which is the part worth testing hardest
Ask for the same resolution twice. The second call succeeds, and there is still exactly one settlement record. That is what a safe retry looks like from your side, and it is the behaviour to build against. Ask for a different resolution and you get 409 resolution_conflict rather than a rewritten settlement: a position that was going to win and now loses is a contradiction, not a retry.
These name a position, never a market
Markets are a shared catalogue. Resolving one would settle every other operator's positions on it, so a forced outcome addresses a single position of yours and nothing else. Another tenant's position id answers 404, identically to an id that does not exist.
When the sandbox has nothing left to teach you
The move to a live key has its own page: Going live gives the sequence to work in, the checks to pass first, and what differs once the tenant is live. Everything you proved here about idempotency, recovery, settlement and error handling carries over unchanged.

