Environments: sandbox and production
There is one money API. The same routes, request bodies, response shapes and nearly all error codes serve both environments; what sits behind them differs. In the sandbox the rail is a simulated bank that books against a test ledger. In production it is our regulated banking partner, and money moves over real schemes.
You switch environment by changing two strings:
| Sandbox | Production | |
|---|---|---|
| Base URL | https://api.sandbox.cashxchain.com | https://api.cashxchain.com |
| Secret key | cxc_sk_sandbox_… | cxc_sk_live_… |
| Developer console | dev.sandbox.cashxchain.com | dev.cashxchain.com |
| Sign up / sign in | auth.sandbox.cashxchain.com | auth.cashxchain.com |
| Web app | app.sandbox.cashxchain.com | app.cashxchain.com |
The environments are separate deployments with separate databases. A key from
one is unknown to the other and is refused with the same 401 unauthorized
as any unknown key. The sandbox/live segment in the key exists so that you
and your logs can tell the two apart at a glance — see
key format. Keys in the older format
(sk_sandbox_…, without cxc_) say sandbox in every environment: one of
those may be a production key.
What is identical
- Paths and methods. Every
/v1/banking/*route,/v1/fx/rates, the webhook, key and log routes. - Request and response shapes. Field names, types and the error envelope.
- Credentials and access rules. Which key type may call which route, scopes, step-up.
- Plan limits, and the
Idempotency-Keyrules for payouts and conversions.
Validation is not identical: the sandbox rail checks some things production leaves to the partner, and the reverse — see the tables below.
What exists only in the sandbox
| Route | Purpose |
|---|---|
POST /v1/sandbox/banking/faucet | Credit test money to a bank account — see Test money. A session, or a key with payments:write |
POST /v1/sandbox/banking/kyb/digital-assets/approve | Approve your account with the crypto-asset service provider's sandbox, so a digital-asset account can be opened — see Onboarding. A session, or a key with accounts:write |
POST /v1/sandbox/banking/accounts/{account_id}/simulate-deposit | Test funds into a digital-asset account — see Test funds. A session, or a key with payments:write |
POST /v1/sandbox/banking/payouts/{payout_id}/simulate-arrival | Book a send-back's EUR on your sandbox EUR account now — see Send back. A session, or a key with payments:write |
POST /v1/sandbox/banking/payouts/{payout_id}/simulate-completion | Settle now a send the provider's sandbox leaves in flight — to an IBAN or a bank account, a send-back, a swap — see Settling a send now. A session, or a key with payments:write |
POST /v1/sandbox/banking/adopt-corporate | Attach a partner-approved test company to your account (session only) |
GET /v1/openapi.json, GET /v1/docs | The machine-readable spec and Swagger UI |
/v1/sandbox/admin/* | Internal sandbox tooling — not part of the API |
Production answers 404 for all of them.
Digital assets are sandbox-only. Digital-asset accounts, wallets, sends,
trusted beneficiaries and the routes above exist in the sandbox; production
offers none of them yet. Their sandbox talks to our crypto-asset service
provider's own sandbox, so its timing differs from the simulated bank's: a
digital-asset send is submitted in the 202, not settled, and the sends
that sandbox leaves in flight are settled by ours from a minute after
acceptance (five minutes for a us_bank send), once the money has been seen
leaving the wallet (simulated: true; it stays processing until then). See
Sandbox behaviour.
/v1/openapi.json exists only on the sandbox host. Use
https://api.sandbox.cashxchain.com/v1/openapi.json as the machine-readable
contract for both environments — the routes it describes are the same. The
spec is incomplete in places; Route reference
lists what it is missing.
What differs: the rail, and therefore timing
The sandbox rail answers everything inside the request. Production waits for a bank. A client that has only ever run against the sandbox will treat "accepted" as "done" — design for the production column from day one.
| Behaviour | Sandbox | Production | Read more |
|---|---|---|---|
| Verification (KYB) | A referral is approved immediately. Payouts and conversions are also allowed while the account is still pending. | The partner reviews the application; approval takes days. Payouts and conversions are refused (403) until the account is active. | KYB |
| Opening an account | Opened on demand, bank details in the 201. | EUR, GBP, USD and CAD are opened by the partner when you are approved. Other currencies come back requested and appear when the partner opens them. | Accounts |
| Payee-name check | No check; always not_checked. | Checked for most GBP (Confirmation of Payee: Faster Payments and CHAPS) and EUR (Verification of Payee) destinations. A destination the check does not cover, or a bank that does not answer, reads not_checked and passes. A mismatch blocks the payout until the payer has seen it; invalid blocks it outright. | Beneficiaries |
| Corridor requirements | A fixed simulated table; payment_purposes and reference_max_length are always empty. | The partner's live requirements, per entity type and payment method. | Beneficiaries |
| Payout amount and reference | More decimal places than the currency has is refused; reference length is not checked. | The amount goes to the rail unchecked; reference is at most 18 characters. | Payouts |
| Paying from an account in another currency | Converted directly at the sandbox rate. | Refused without an fx_quote_id. | Payouts |
| Payout status | settled in the 202 response — except a send from a digital-asset account, which is submitted and moves as the crypto-asset provider's sandbox reports it; those that sandbox leaves in flight are settled by ours from a minute after acceptance (five minutes for a us_bank send), once the money has been seen leaving the wallet (it stays processing until then). | submitted in the 202, then processing and settled or failed as the partner reports — minutes to days. | Payouts, Sandbox behaviour |
| FX conversion | settled in the response. | booked; settles later. | FX |
| FX quote lifetime | 15 minutes. | Set by the partner per quote — always read expires_at. | FX |
| Beneficiaries | Registered instantly; the same Idempotency-Key returns the same beneficiary. | Registered at the partner; a retried create can register a duplicate. | Beneficiaries |
| Balances | The test ledger, exact and immediate. | Read from the partner, cached briefly; refreshing: true means a newer figure is being fetched. | Accounts |
| Payout quote | Prices the payout with your plan's payment fee and the sandbox rate (simulated: true). | No fee. Same currency is rate: "1"; a conversion uses the partner's rate. | Payouts |
| Payment emails | Not sent. | "You sent" / "you received" emails to the account's address. | — |
A few error codes are rail-specific
The codes your code branches on are the same in both environments, with these exceptions, which come from where the check runs:
| Situation | Sandbox | Production |
|---|---|---|
| Not enough money for a payout or conversion | 400 insufficient_balance | Checked by the partner, not by us: normally accepted and later failed; a request it rejects outright is 400 bad_request |
| Executing an expired or already-used FX quote | 400 quote_expired / 400 quote_consumed | Checked by the partner, as above |
| A payee name that does not match | Cannot happen — no check | 422 payee_name_mismatch |
Paying from an account in another currency without fx_quote_id | Allowed | 400 bad_request |
| No rate for the pair; a fee that cannot be priced; a conversion amount that is not positive | 422 no_rate; 503 fee_unpriceable; 400 invalid_amount | Not returned — the partner prices |
| Accepting a payee-name verdict | 200 with accepted: false and outcome: "not_checked" — the sandbox rail has no name check, so there is nothing to accept | 200 with the acceptance recorded; 422 payee_name_mismatch for an invalid destination |
| A corridor nobody offers | 422 corridor_unsupported from the simulated table | 422 corridor_unsupported from the partner's requirements; a rail that is down is 503 service_unavailable |
In production, write your handling for the generic bad_request and for a
payout or conversion that turns failed after it was accepted, and show the
payer the message. See Errors.
Test money
POST /v1/sandbox/banking/faucet credits a currency account you already hold.
A session, or an API key with payments:write — a secret or master key, or a
restricted key holding it.
POST /v1/sandbox/banking/faucet
Authorization: Bearer <session token or API key>
Idempotency-Key: faucet-eur-0001
Content-Type: application/json
{"currency": "EUR", "amount": 100}
{"currency": "EUR", "amount": "100", "balance": "160.0000"}
amountis whole units, 1 to 100,000.409 conflict— "you hold no active EUR account — open one first".400 bad_request— amount out of range or fractional, or anIdempotency-Keythat is empty, over 255 characters or not visible ASCII.422 asset_unsupported— a digital asset (USDC,EURC, …): the faucet credits bank accounts only. Fund a digital-asset account withsimulate-deposit.429 rate_limited— more than 30 requests an hour from your account.
Idempotency-KeyIt is optional here, and worth sending. Under one the credit is made once: a
repeat — a retry after a timeout — answers 200 with the same currency and
amount and the balance as it stands, and credits nothing; the key again with
another currency or amount is 409 idempotency_key_reuse. Without one,
every call credits again — do not retry a keyless call blindly.
In the web app: Accounts → Get test funds. A faucet credit appears in
GET /v1/transactions as a faucet entry and is immediately spendable.
Moving to production
- Complete verification for your production account and wait for it
to become
active. - Create a production secret key in dev.cashxchain.com.
It starts with
cxc_sk_live_. - Change the base URL and the key.
- Re-read the timing table above and check that nothing in your code assumes a payout is final when it is accepted.
- Fund your account by bank transfer to the details in
GET /v1/banking/accounts/{account_id}/payin-instructions.
Never use a live key in local development, and never commit either kind of key.