Sandbox
Sandbox and test cards
Every card number the simulator understands, the metadata keys that override them, and how the challenge page and the async rails behave.
Updated
Any cp_*_test_ key routes to the simulator. The table below is rendered from the same module the simulator reads, so it is the behaviour, not a description of it. The endpoints themselves are documented under sandbox endpoints.
Magic cards
Tokenise one of these with POST /v1/sandbox/tokens, then create a payment with the token. Any future expiry and any 3 or 4 digit security code are accepted; a number that is not in this table is refused with invalid_card_number.
| Card number | Brand | Resulting status | What the simulator does |
|---|---|---|---|
| 4242 4242 4242 4242 | visa | captured | Authorised, then captured when capture_method is automatic. |
| 4000 0000 0000 0002 | visa | declined | decline_code generic_decline, decline_is_soft false — do not retry with this card. |
| 4000 0000 0000 9995 | visa | declined | decline_code insufficient_funds, decline_is_soft true — safe to retry. |
| 4000 0000 0000 0069 | visa | declined | decline_code expired_card, decline_is_soft false — do not retry with this card. |
| 4000 0000 0000 0127 | visa | declined | decline_code incorrect_cvc, decline_is_soft false — do not retry with this card. |
| 4000 0000 0000 0119 | visa | failed | 422 processing_error with code payment_failed; response code 96. |
| 4000 0000 0000 3220 | visa | requires_action | next_action.redirect to the simulated challenge page, then approved. |
| 4000 0000 0000 3063 | visa | requires_action | next_action.redirect to the simulated challenge page, then declined. |
| 4000 0000 0000 0259 | visa | processing | Resolves to captured after 30 s, or immediately on POST /v1/sandbox/advance/:id. |
The same rows are available as JSON from GET /v1/sandbox/test-cards, so a test fixture can be generated rather than copied.
Metadata overrides
The card decides the outcome for card payments. To force an outcome on any method — including alternative methods, which have no card number — put one of these keys in metadata on the payment. They are ignored outside test mode.
| metadata key | Accepts | Effect |
|---|---|---|
| _cp_test_outcome | approved · declined:<decline_code> · failed · requires_action | Overrides the card's planned outcome. With declined:<decline_code> the soft codes are insufficient_funds, issuer_unavailable, try_again_later, sca_required; every other code is hard. |
| _cp_test_latency_ms | integer | Delays the connector response by that many milliseconds before it answers. |
| _cp_test_http_error | HTTP status | Answers with that status and code connector_http_error instead of processing. The payment stays pending. |
curl https://cleared-pay.com/api/v1/payments \
-H "Authorization: Bearer $CLEARED_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 2000,
"currency": "eur",
"reference": "ORDER-10432",
"payment_method": { "type": "ideal" },
"return_url": "https://merchant.example/return",
"metadata": { "_cp_test_outcome": "declined:stolen_card", "_cp_test_latency_ms": 800 }
}'The simulated challenge page
A card that requires authentication answers with status: "requires_action" and a next_action.redirect.url pointing at /3ds-test/:sessionId. The session id is the connector's own transaction reference, so you can correlate the two. The page has three buttons:
- Approve runs the card's planned outcome — so
4000 0000 0000 3220captures and4000 0000 0000 3063declines. - Decline records a soft
sca_requireddecline withthree_ds.status: "failed". Soft, because a fresh challenge is the correct retry. - Timeout fails the payment: status
failed, not declined, because the issuer never answered.
After any of the three the payer is returned to your return_url. Read the payment back when they land — the redirect is not proof of an outcome.
Alternative methods
Methods have no magic numbers; they have rails, and each rail behaves the way it does in production.
- Redirect rails — iDEAL, Bancontact, BLIK, Przelewy24, Trustly, PayPal, Klarna, MB WAY, open banking, the e-wallets, vouchers and crypto — go straight to
requires_actionwith a redirect. Follow it, and the payment resolves. - Async rails — SEPA Direct Debit, SEPA Credit Transfer, Multibanco and Pix — stay
processingfor 30 seconds and then capture, which is the shape of a real bank transfer without the wait. - Wallets — Apple Pay and Google Pay need a real device flow. They are not simulated, and the hosted checkout page leaves them out in test mode rather than faking a result.
No method needs a token. Set payment_method.type and a return_url, and use the metadata overrides above to force a decline or a failure.
Advancing and simulating
Waiting is the slowest thing in a test suite, so nothing needs waiting for. Force a processing payment to resolve, and fire an event at your receiver without making a payment at all:
# resolve an async payment now instead of in 30 s
curl -X POST "https://cleared-pay.com/api/v1/sandbox/advance/pay_01JBXQ6P9R4T2V7XZ1BD5GH8KM" \
-H "Authorization: Bearer $CLEARED_SECRET_KEY"
# store a real, signed event and deliver it inline; the attempts come back in the response
curl https://cleared-pay.com/api/v1/sandbox/simulate-webhook \
-H "Authorization: Bearer $CLEARED_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"payment.captured","resource":"pay_01JBXQ2M4K7T9V3ZC8HR5N6PWD"}'advance answers 422 payment_not_advanceable for a payment that is not processing. A simulated event is stored and signed like any other, so it is the right way to test verification — including the rotation case, where the header carries two v1= values.