Sandbox behaviour
The digital-asset sandbox — https://api.sandbox.cashxchain.com and
app.sandbox.cashxchain.com — talks to
our crypto-asset service provider's own sandbox. Nothing real moves: no real
or testnet asset, and no bank transfer, is ever credited to a sandbox wallet
or IBAN. Read this page before you test, so that what the provider's sandbox
does not do is not mistaken for a fault.
How money gets in
| Way in | In the sandbox |
|---|---|
Test funds — POST /v1/sandbox/banking/accounts/{account_id}/simulate-deposit, or Add test funds in the web app | The way to fund a digital-asset account. Booked within seconds: balance, a transaction, payin.received |
| An on-chain transfer to one of your addresses | Never arrives. Do not send real or testnet assets to a sandbox address |
| EUR by SEPA to the USDC account's IBAN | Never arrives, and cannot be simulated: the provider's sandbox refuses it |
| A convert from your sandbox EUR account, where offered | The estimated USDC is deposited into your Solana wallet and booked within seconds |
| A send by @handle from another sandbox organisation | Arrives — see below |
The faucet (POST /v1/sandbox/banking/faucet) credits bank accounts only: a
digital asset is 422 asset_unsupported. It takes a session or an API key
with payments:write, so a key-only integration can fund the EUR account a
convert starts from — see Test money.
Credits held as a possible refund
Test funds are always credited, whatever you sent before: they are recorded before the provider is asked, and the deposit they become is booked as yours.
Money back from one of our own payouts is never booked as a pay-in. Any other credit that names no transfer of ours — not test funds, not EUR through the USDC account's own IBAN, not a convert — is therefore held as a possible refund when it is 90 % to 105 % of what a send from the same wallet took in the last 14 days:
| That send | Counts |
|---|---|
To a bank account — an IBAN, us_bank, gb_bank, clabe, pix, a send-back | Settled or not: a bank can return a payment after it settled |
| On chain, by @handle, a swap | Until it settles: one that fails after taking the money, or comes back, still counts |
Held, the credit shows no transaction and no payin.received, and it needs an
operator to release it — even though the wallet's live balance from the
provider may already show it.
Which sends finish
| Send | In the sandbox |
|---|---|
| On chain, same chain | settled with a tx_hash once the provider's sandbox completes it — when |
| Across chains | settled with a tx_hash, as on the same chain |
| By @handle | settled as on chain, with no tx_hash (by design) |
| To an IBAN | Settled by the sandbox at the earliest a minute after the provider accepts it, and only once the money has been seen leaving the wallet — below |
gb_bank (Faster Payments), clabe (SPEI), pix | Settled by the sandbox, as to an IBAN |
us_bank (ACH, wire) | Left to the provider's sandbox for five minutes. It was seen to complete an ACH send itself, within 168 seconds of accepting it (2026-11-03); a send it completes reads settled with no simulated. One still in flight after five minutes — a wire, not yet observed, or an ACH send it has not completed — is settled by the sandbox, as to an IBAN. simulate-completion settles one at once, whatever its wait |
| Swap | Settled by the sandbox, as to an IBAN, once the provider's sandbox has been asked to deposit the bought asset into the wallet. The deposit shows as the swap's arrival once the wallet is read, within seconds |
| Send back to your EUR account | Settled by the sandbox, as to an IBAN. Its arrival is simulated too: the EUR is booked on your sandbox EUR account within about two minutes, or at once with simulate-arrival |
| Convert from your sandbox EUR account, where offered | settled in the 202; the USDC arrives within seconds |
Poll GET /v1/banking/payouts/{id} for a send's status; there is no webhook
for it.
When a send reads settled
The public sandbox receives no webhooks from the provider, so what moves a send is the sandbox's own reads:
- Its status, from the provider: every 15 seconds for the first ten minutes after the provider accepted it — up to 10 payouts a read across the whole sandbox, the least recently read first, so with more in flight each is read every few reads — and about every two minutes while it is in flight. While the provider throttles the 15-second read or does not answer, that read pauses, for up to two minutes; the two-minute read goes on.
- Its wallet's history, where the provider's debit shows: whenever a status read finds the provider has moved the send; for a send the sandbox settles, 15 seconds before its wait is over and, while the debit is not seen, at the wait and 30 seconds, 1½, 3½ and 7½ minutes after it; and at least every five minutes. Each read comes within about five seconds of being asked for, or once the round of reads under way has finished.
| Send | Reads settled |
|---|---|
| On chain, across chains, by @handle | At the first status read after the provider's sandbox completes it. In our runs it completed them from about 25 seconds to a little over a minute after accepting them, and in one run between about 45 seconds and 3½ minutes after |
To an IBAN, gb_bank, clabe, pix, a send-back, a swap | When the sandbox settles it: with the default one-minute wait, 60 to 75 seconds after acceptance where the provider has listed its debit by about 45 seconds; otherwise within about 15 seconds of the first read of the wallet after the debit is listed. When the provider takes the money, it does not say |
us_bank | At the first status read after the provider's sandbox completes it, where it does within five minutes. Otherwise when the sandbox settles it: by default 300 to 315 seconds after acceptance where its debit is listed by about 285 seconds |
Any of the sends the sandbox settles, with simulate-completion | At once, whatever its wait, once its debit has been seen |
These are the sandbox's own reads; when the provider's sandbox completes a send, or lists its debit, it does not say in advance.
Sandbox addresses and tx_hash values are not on any public chain: neither
can be looked up on an explorer.
Sends the sandbox settles
The provider's sandbox accepts a send to an IBAN or to a bank account outside
SEPA, a send-back and a swap, and then leaves it in flight — for good, except
that it was seen to complete a send over ACH itself (us_bank, above). In
production these settle. So that every send reaches settled in the sandbox
too, the sandbox settles them itself:
- Only once the money has left the wallet: once the wallet's history
shows the debit naming the send's transfer, read as
above. Until then the send stays
processing. - From a minute after the provider accepted it — five minutes for a
us_banksend, so that the provider's own completion of an ACH send comes first — once that debit has been seen: at the first run of the sandbox's settling pass after both, which runs every 15 seconds.simulate-completionskips the wait but not the debit: until the debit is seen it answers409 debit_not_seen. - Only where the provider left it. A send the provider has moved on —
completed, on its way back, refunded, or in a state support resolves —
follows the provider, as everywhere. The sandbox settles a send only while
it is still
submittedorprocessing: one support is resolving (needs_reconciliation) is left to support, and one the provider completes meanwhile readssettledwith nosimulated. - A swap's bought asset is deposited by the provider's sandbox first: the
amount the provider's receipt states, else the estimate a quote of the swap
shows (
destination_amount_estimate), cut to the cent. It is booked into your account in the asset bought and shown as the swap'sarrival(matched_by: "transfer") once the wallet is read, within seconds of the settlement: the200may still showarrival: null, so read the payout again.GET /v1/transactionslists it asinternalwith no counterparty — not as a deposit on chain, though the provider's sandbox records it like one. It is deposited once, however often the completion is tried. - Settled as any payout:
statussettledwithsettled_at, andsimulated: trueon the payout. The field is absent on every payout the sandbox did not settle itself, and always in production. Its hold is released as any finished payout's is: for a send whose balance is read a minute or more after the settlement. - What left, on a send-back. In production a send-back shows the
provider's receipt of it:
source_amount,source_currencyandexchange_rate. The provider's sandbox never sends one, so a send-back settled here shows what the sandbox saw in their place:source_amountandsource_currency, the USDC the wallet has been seen to give for its transfer (every debit naming the transfer, summed:"5.640000"USDCfor 5.00 EUR), andexchange_rate, the EUR that arrived per USDC that left —amount÷source_amountto six places ("0.886525"), as the provider defines its receipt's rate. The figure is worked out each time the payout is read: the send-back settles on the first debit, and an entry for its gas read later counts too. No fee is stated, and what the wallet gave need not be the figure a receipt states in production. A send-back whose conversion the provider did report shows the provider's figures, also where that report is recorded after the settlement; one with a debit naming its transfer whose amount could not be read shows none of the three. - Not on the other sends settled here. A send to an IBAN or to a bank
account outside SEPA fixes what leaves — its
amount, in the asset — and only the provider's receipt states what the bank was paid: in the sandbox it carries nosource_amountorexchange_rate. Nor does a swap: what left is itsamount, what it bought itsarrival, and the provider states no rate for a swap.
Never touched: a send on chain, across chains or by @handle (the provider's sandbox completes those itself — when), and the convert (settled at once).
Settling a send now
POST /v1/sandbox/banking/payouts/{payout_id}/simulate-completion settles one
of the sends above at once, whatever its wait — a minute, or five for a
us_bank send; its debit must still have been seen. A session, or a key with
payments:write. No body and no Idempotency-Key: the payout is the key.
curl -s -X POST "$CXC_BASE/v1/sandbox/banking/payouts/$PAYOUT_ID/simulate-completion" \
-H "Authorization: Bearer $CXC_KEY" | jq '{status, settled_at, simulated, arrival}'
200 with the payout, as GET /v1/banking/payouts/{id} shows it: settled by
this request or an earlier one (simulated: true), or finished before —
settled by the provider, failed, cancelled or returned — and left as it is.
Asking again does nothing twice.
| Status | error | When |
|---|---|---|
404 | not_found | Not one of your payouts |
409 | not_simulatable | details.reason: debit_not_seen — the money has not yet been seen leaving the wallet; the wallet is read again at once, so ask again in a few seconds. not_accepted — the provider has not accepted it. needs_reconciliation — support is resolving it. provider_state — the provider reports it in a state its sandbox moves on from (details.partner_status): it follows the provider. A us_bank send the provider's sandbox has completed answers this until the next read of its status records the completion; then 200, settled with no simulated. wallet_not_open, asset_not_on_network — a swap whose bought asset cannot arrive: no wallet carrying it is open on the chain, or it is no longer offered there. arrival_not_seen — a swap whose bought asset was asked for over 23 hours ago and never seen: it stays processing for support |
422 | not_eligible | details.reason: not_a_digital_asset_send; completes_at_provider — on chain, across chains or by @handle; no_casp_layer, no_digital_rail, provider_not_sandbox — this deployment settles nothing itself |
429 | rate_limited | More than 30 requests an hour from your account; or the provider's sandbox throttled the read (the message says upstream partner rate-limited the request): ask again shortly |
503 | service_unavailable | Being completed by another request at that moment, the provider could not be read (a provider timeout can also be 504 gateway_timeout), or its sandbox did not take a swap's bought asset. Ask again shortly: it is settled, and what a swap bought deposited, once |
Holds of sends in flight
While a send is in flight, and for about a minute after it finishes, its wallet holds what it may still take:
- Until the wallet's history shows the debit naming its transfer: the
whole send — its amount and its chain's headroom (1 %; 5 % on Ethereum), a
send-back its whole
max_source_amount. - From a minute after that debit is read: only the headroom above it, until the send settles, fails or comes back.
- For about a minute after it finishes: what is left of its hold. Each send reads the wallet's balance afresh, and that read may not show the finished send's last debit yet. A send whose balance is read less than a minute after another finished still counts what is left of that one's hold — its headroom, or all of it where its debit was first seen less than a minute before the read. A send read a minute or more after counts none of it.
So from the provider's debit until a minute after the wallet read that sees
it, the balance and the hold count the same money twice, and what is left of
a hold counts for about a minute after its send finishes: available in the
refusal is that much lower meanwhile, and a send refused then fits once the
minute is over, under a new Idempotency-Key. The wallet is read within
seconds of a status read that finds the provider has moved a send — every 15
seconds for a send's first ten minutes, above —
and at least every five minutes.
A send refused when its transfer was created (below)
holds nothing. A later send can be refused 400 insufficient_balance sooner
than the balance suggests. The message names what is free and what the send
needs, the amount and the headroom apart:
Insufficient balance: available 9.10 USDC, needed 10.10 USDC: 10.00 USDC for the payout plus 0.10 USDC headroom, held while it is in flight
On a send-back, the amount is the USDC estimate of the EUR it sends, and the
headroom the rest of its max_source_amount. When testing several sends in a
row from one wallet, add test funds to spare.
A refusal when the transfer is created
Some refusals come from the provider only when the transfer is created — a
Stellar account with no trust line for the asset is one. Refused in answer to
the only request sent, such a send answers 422 destination_not_payable with
details.reason: provider_refused, recorded: details.payout_id,
details.idempotency_key_spent: true. The payout is failed at once and
holds nothing, and your next send is not held behind it.
- A refusal of what you sent carries the provider's sentence in the
message, and names the field in
details.provider_fieldwhen the provider named one, as it names it (destination_stellar_account). Change it and send again under a newIdempotency-Key. - Any other refusal — a permission or compliance refusal, an object it
does not know, a rail it will not use — is in our words: the provider
refused this payout for this account; contact support before sending it
again under a new key. A rail refusal still names
details.provider_field: "destination.payment_rail": tell the two kinds apart by the message, not by that field.
The provider refusing our own credentials at that point is
503 service_unavailable, recorded the same way: ours to fix, the payout
failed, the next send not held behind it; send again later under a new key.
Anything else at the create — no answer, an outage, a rate limit, a conflict,
or a refusal after a retried attempt — is still 503 payout_outcome_unknown.
Send it again only under the same key. Until it reads submitted or failed
(seven minutes or more), your other sends answer
409 earlier_payout_unconfirmed. See Errors.
To avoid a refusal of an on-chain send, quote first. Where the deployment runs
the provider's dry run, the quote answers valid: false with the provider's
reason — see Quotes. Fix what
it names before sending. A quote to a bank account, or of a swap, is an
estimate only and does not foresee a refusal.
An @handle arrival
A send by @handle is booked once, on the recipient's account, as an internal
payment from the sender: type and rail internal, and the sender's name
as their handle shows it (counterparty.name, payer_name) — never the
sender's wallet. The provider's sandbox records the money reaching the
recipient's wallet as a plain deposit that does not name the send, so it is
matched to the send by the wallet, the asset and the exact amount — each send
once.
Test funds of exactly that amount, simulated into the same wallet while the send is on its way, may be shown as the send, and the send's own arrival as the test funds. Amounts are right and nothing is booked twice; to keep the two apart, give test funds an amount unlike any send by @handle on its way to you.
Regions
The provider decides routes by your business's country, the one your account was opened with:
| A business in the EEA | A business outside the EEA | |
|---|---|---|
USD to a US account (us_bank) | Yes | Yes |
GBP, MXN, BRL (gb_bank, clabe, pix) | No — 422 corridor_unsupported, and approving such a trusted beneficiary is refused too | Yes |
| USDT across chains, and by @handle | No | Yes |
digital_actions and GET /v1/banking/capabilities show what your account is
offered. rails in GET /v1/banking/kyb/digital-assets shows which payout
rails the provider has enabled for you: a bank send is offered only over an
enabled rail. The sandbox approval
requests and enables all of them. An account approved before GBP, MXN and BRL
were added holds USD and EUR (SEPA) only: call the approval again to add them,
and check rails.
Webhooks
| Event | In the sandbox |
|---|---|
payin.received | Sent for each credit booked on your accounts: test funds, a convert's USDC, a send-back's simulated EUR arrival, a held credit booked once you open its asset, a send by @handle, a swap's bought asset. data: payin_id, amount, currency (the asset), rail (the chain, such as solana; sepa for a bank credit; internal for a send by @handle and a swap's bought asset), payer_name (null on chain and for a swap's bought asset; the sender's name for a send by @handle), reference, booked_at. It does not name the account: an asset has one account, so currency identifies it |
| Payout events | None. No event for any send's status — poll GET /v1/banking/payouts/{id} |
No payment mails are sent in the sandbox. See Webhooks.
Limits
- Sandbox tools —
simulate-deposit,simulate-arrival,simulate-completion, the sandbox approval and the faucet each take at most 30 requests an hour from your account, a refused request included (429 rate_limited, the message says how many minutes are left). The sandbox convert has a window of its own. - The approval and test funds call the provider's sandbox on a key every tester shares: a script looping on them is throttled there too.
- Send caps, per asset: 10,000 a send, 50,000 a day, 250,000 a month.
- Rates in the provider's sandbox can be months old: show
rate_as_offrom a quote.
What the sandbox does not do for digital-asset accounts
- Statements — not available for digital-asset accounts
(
400 bad_request, naming the account's asset). UseGET /v1/transactionsfor credits andGET /v1/banking/payoutsfor sends. - Payout webhooks — none; poll.
- Production — none of this is offered in production yet: there, digital-asset accounts, wallets and sends answer as Capabilities describes.