Sandbox

Test keys, simulated rails, and amounts that fail on purpose.

Sandbox is not a separate deployment. It is the same API, the same code paths and the same validation, reached with a key that begins sk_test_.

The two worlds do not touch

A test key sees test data. A live key sees live data. Neither can see the other, and that includes:

  • charges, payouts and webhook endpoints — filtered by which key asked
  • idempotency keys — separate namespaces

That last one matters more than it sounds. If they shared a namespace, the order references you used while building would collide with your real ones the first time you re-ran your integration tests against production — and we would hand you back the test charge and tell you it had succeeded.

Amounts that fail on purpose

amount_minorOutcome
999999declined
999998timed out
999997succeeded, then reversed
999996pending review

Anything else succeeds.

An amount works for a charge and for a payout, needs no setup, and survives being pasted into a form. The values are far enough from anything real that nobody trips one by accident — 999999 minor units is 9,999.99, and a business testing with round numbers will not land on it.

Destinations that fail on purpose

An amount is not always yours to choose — a payout against a saved beneficiary is for whatever the invoice says. So there are recipients that ask for an outcome too:

DestinationOutcome
+250788000001succeeds
+250788000002declined
+250788000003timed out
+250788000004succeeded, then reversed
+250788000005pending review

These are inside Rwanda's reserved test range, so they cannot ring.

The destination wins where both are magic. Somebody who has gone to the trouble of naming a failing recipient means it more than they mean whatever amount they happened to type.

What a failure says

The simulated rail reports a fixed reason per outcome, so you can assert on it:

Outcomefailure_reason
declinedinsufficient_funds
timed outupstream_timeout
reversedreturned_by_beneficiary
pending reviewmanual_review_required

Match on the outcome and the reason, never on the message. Reasons from a real rail are normalised into one vocabulary — insufficient_funds, invalid_account, cancelled_by_payer, timeout, rejected_by_provider, account_unavailable, unknown — so you never learn four partners' vocabularies, and the human-readable text beside them may change at any time.

Balances do not move

Sandbox charges and payouts do not affect any real balance, including the test one. Nothing here reconciles against money.

On this page