Skip to main content

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:

SandboxProduction
Base URLhttps://api.sandbox.cashxchain.comhttps://api.cashxchain.com
Secret keycxc_sk_sandbox_…cxc_sk_live_…
Developer consoledev.sandbox.cashxchain.comdev.cashxchain.com
Sign up / sign inauth.sandbox.cashxchain.comauth.cashxchain.com
Web appapp.sandbox.cashxchain.comapp.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-Key rules 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​

RoutePurpose
POST /v1/sandbox/banking/faucetCredit test money to a bank account — see Test money. A session, or a key with payments:write
POST /v1/sandbox/banking/kyb/digital-assets/approveApprove 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-depositTest 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-arrivalBook 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-completionSettle 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-corporateAttach a partner-approved test company to your account (session only)
GET /v1/openapi.json, GET /v1/docsThe 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.

The OpenAPI spec is not served in production

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

BehaviourSandboxProductionRead 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 accountOpened 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 checkNo 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 requirementsA 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 referenceMore 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 currencyConverted directly at the sandbox rate.Refused without an fx_quote_id.Payouts
Payout statussettled 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 conversionsettled in the response.booked; settles later.FX
FX quote lifetime15 minutes.Set by the partner per quote — always read expires_at.FX
BeneficiariesRegistered instantly; the same Idempotency-Key returns the same beneficiary.Registered at the partner; a retried create can register a duplicate.Beneficiaries
BalancesThe test ledger, exact and immediate.Read from the partner, cached briefly; refreshing: true means a newer figure is being fetched.Accounts
Payout quotePrices 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 emailsNot 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:

SituationSandboxProduction
Not enough money for a payout or conversion400 insufficient_balanceChecked 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 quote400 quote_expired / 400 quote_consumedChecked by the partner, as above
A payee name that does not matchCannot happen — no check422 payee_name_mismatch
Paying from an account in another currency without fx_quote_idAllowed400 bad_request
No rate for the pair; a fee that cannot be priced; a conversion amount that is not positive422 no_rate; 503 fee_unpriceable; 400 invalid_amountNot returned — the partner prices
Accepting a payee-name verdict200 with accepted: false and outcome: "not_checked" — the sandbox rail has no name check, so there is nothing to accept200 with the acceptance recorded; 422 payee_name_mismatch for an invalid destination
A corridor nobody offers422 corridor_unsupported from the simulated table422 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.

HTTP
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}
JSON
{"currency": "EUR", "amount": "100", "balance": "160.0000"}
  • amount is 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 an Idempotency-Key that 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 with simulate-deposit.
  • 429 rate_limited — more than 30 requests an hour from your account.
Send an Idempotency-Key

It 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​

  1. Complete verification for your production account and wait for it to become active.
  2. Create a production secret key in dev.cashxchain.com. It starts with cxc_sk_live_.
  3. Change the base URL and the key.
  4. Re-read the timing table above and check that nothing in your code assumes a payout is final when it is accepted.
  5. 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.