Capay Global

Sandbox

Every account gets a full sandbox. Sandbox traffic runs against a payment simulator: it never touches real providers and no real money can move, while behaving exactly like production, including fees, webhooks and reconciliation.

Get your test keys

Create a free account, then open Developers > API keys in the dashboard. Sandbox keys start with sk_test_ and are shown once, so store the key the moment it appears. Sandbox keys never touch a real provider and never move real money.

Sandbox keys

Use your sk_test_ secret key. The same endpoints, envelopes and error catalog apply in both environments; only the key decides where you are.

header
Authorization: Bearer sk_test_XXXXXXXXXXXX   # sandbox
Authorization: Bearer sk_live_XXXXXXXXXXXX   # production

Try it

Price a 5,000 XAF mobile money collection in Cameroon. This is safe to run as often as you like:

curl
curl "https://api.capayglobal.com/v1/pricing?direction=collection&country=CM&currency=XAF&method=mobile_money&amount=5000" \
  -H "Authorization: Bearer sk_test_XXXXXXXXXXXX"

Choosing an outcome

The simulator picks a scenario in this order. The X-Scenario-Key header wins whenever it is present; otherwise the last four digits of the MSISDN are looked up; otherwise you get success.

scenario resolution order
X-Scenario-Key: failed.insufficient_funds   # 1. wins whenever it is present
+237650000002                              # 2. magic suffix 0002, used when no header was sent
success                                    # 3. the default for every other number

The header is strict: an unknown key, or a key that does not apply to the resource you are creating, answers 422 with a validation_errors entry for X-Scenario-Key. It is ignored entirely on an sk_live_ key, so leaving it in production code changes nothing.

curl
curl -X POST https://api.capayglobal.com/v1/collections \
  -H "Authorization: Bearer sk_test_XXXXXXXXXXXX" \
  -H "Idempotency-Key: col-order-10483-d4e5f6" \
  -H "X-Scenario-Key: failed.insufficient_funds" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "5000",
    "currency": "XAF",
    "country": "CM",
    "method": "mobile_money",
    "msisdn": "+237650000001",
    "merchant_reference": "order-10483"
  }'

Test scenarios

A scenario scripts the whole lifecycle of one transaction: which statuses it passes through, after how many seconds and with which reason_code. The timings are what the simulator waits before each step, so a run is deterministic without being instant. Direction says which resources a scenario applies to.

Scenario keyDirectionTimelineWhat it is for
successanyprocessing 4s, completed 8sThe happy path, and the default when nothing else matches.
success.lateanyprocessing 4s, completed 90sSlow finality: exercises your pending-state screens and your patience with requery.
failed.insufficient_fundsanyprocessing 4s, failed 8s (insufficient_funds)The payer's balance was too low.
failed.invalid_customeranyfailed 4s (invalid_customer), no processing phaseAn unknown or unregistered number, rejected before any processing phase.
failed.provider_erroranyprocessing 4s, failed 8s (provider_error)An unrecoverable provider-side error.
failed.payer_timeoutcollectionpending until failed 30s (payer_timeout)The payer never answered the prompt. Collections only.
cancelledanycancelled 10s (cancelled_by_payer)The payer declined the prompt.
expiredanyexpired 60s (expired)Nothing happened before the transaction expired.
enqueued.then_successpayoutenqueued 4s (provider_unavailable), processing 24s, completed 28sThe provider is down or the breaker is tripped: the payout is held with the reserve kept, then drains on recovery. Payouts only.
in_reconciliation.then_resolvedanyprocessing 4s, in_reconciliation 8s (amount_mismatch), completed 30sThe amount came back different, a reconciliation item opened, and a human resolved it. Never treat in_reconciliation as failed.
webhook.duplicateanyprocessing 4s, completed 8s, completed again 12s (duplicate)completed is delivered twice. Your receiver must make the second one a no-op.
webhook.out_of_orderanycompleted 4s, processing 8sprocessing arrives after completed. Your receiver must not let it regress the order.

Magic phone numbers

When you send no X-Scenario-Key, the last four digits of the payer or recipient number choose the scenario. This is the easiest way to test a checkout form, where you cannot set a header.

Last 4 digitsExample numberScenario key
0001+237650000001success
0002+237650000002failed.insufficient_funds
0003+237650000003failed.invalid_customer
0004+237650000004failed.payer_timeout
0005+237650000005expired
0006+237650000006enqueued.then_success
0007+237650000007in_reconciliation.then_resolved
0008+237650000008webhook.duplicate
0009+237650000009success.late

Any other number resolves to success. The suffix is a convenience, not a contract: when a suffix maps to a scenario that does not apply to the resource you are creating, the simulator falls back to success rather than failing your request.

Sandbox helpers

POST /v1/test/fund credits your sandbox available balance so payouts and refunds have something to spend. There is a cap per call; over it the request answers 422.

curl
curl -X POST https://api.capayglobal.com/v1/test/fund \
  -H "Authorization: Bearer sk_test_XXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -d '{ "amount": "100000", "currency": "XAF" }'

POST /v1/test/advance makes every pending simulator step for one resource due immediately, so you do not wait 90 seconds for success.late. It answers how many steps it advanced, which is 0 once the resource is terminal. With resource_type subscription it is the subscription test clock: the next payment prompt falls due now and goes out.

curl
curl -X POST https://api.capayglobal.com/v1/test/advance \
  -H "Authorization: Bearer sk_test_XXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -d '{ "resource_type": "collection", "resource_id": "col_7Qm3k1Xa9Zr4" }'

Both helpers exist only in the sandbox. Called with an sk_live_ key they answer 403 sandbox_only: production money is never minted or fast-forwarded.

Test your webhook receiver

Duplicate and out-of-order deliveries are normal in production, so two scenarios reproduce them on demand: webhook.duplicate delivers completed twice, and webhook.out_of_order delivers processing after completed. A receiver that survives both is a receiver that survives production.

The delivery contract and a worked idempotent receiver are in the webhooks guide, and the CLI forwards sandbox events to localhost: Webhooks, CLI.

Handle every status

The sandbox produces every status the production simulator can produce, including enqueued and in_reconciliation. Handle each one explicitly and treat anything you do not recognise as undetermined: keep polling or wait for the webhook, never conclude the payment failed.