Skip to content

Test mode

Every merchant gets two kinds of secret key. The key alone decides the mode — there is no flag to pass.

sk_test_… sk_live_…
Providers a mock connector — never calls Wave or Orange Money Wave, Orange Money
Money none; balances live in separate test: ledger accounts real XOF
Merchant status works while your account is pending_review needs active (else 403 merchant_not_active)
livemode on objects false true

Test and live data never mix: a test payment is 404 for your live key and the other way round, and test money can’t be paid out to a real wallet.

No human needed: the sandbox API opens a test-only account and returns its keys once.

Terminal window
curl -s -X POST https://sandbox-api.xaalis.org/v1/sandbox/signups \
-H "Content-Type: application/json" \
-d '{ "email": "dev@example.com", "business_name": "Boutique Awa", "webhook_url": "https://example.com/webhooks/xaalis" }'
{ "object": "sandbox_signup",
"merchant": { "id": "mer_…", "object": "merchant", "name": "Boutique Awa", "email": "dev@example.com", "status": "pending_review" },
"secret_key": "sk_test_…", "webhook_secret": "whsec_…" }
  • The account has a test key only — it can never move real money. Going live needs a human (identity checks, approval) in the dashboard.
  • Both secrets are shown once: store them in environment variables straight away. With an Idempotency-Key, a replay returns 409 secret_unavailable (with details.merchant_id) — the secrets are never shown twice.
  • Limits: 5 signups per hour and 20 per day per address (429 rate_limited, honour Retry-After). At most 3 sandbox accounts per email, for good: the fourth answers 422 limit_exceeded — retrying won’t help, use an existing account.
  • webhook_url is optional and must be https://. Production (api.xaalis.org) doesn’t have this route (404).
  • AI agents do the same with the MCP tool xaalis_sandbox_signup (AI agents).
To get Do
a successful payment POST /v1/payments/{id}/simulate {"outcome":"succeeded"} — or click Approuver on the test wallet page
a failed payment same with "failed" — or Refuser
an expired payment leave it unpaid; checkouts expire after 30 minutes and are closed by a background job within ~5 minutes after that
a successful payout any recipient phone, e.g. +221770000001
a failed payout a recipient phone ending in 0000, e.g. +221770000000 — the reserved amount and fee are returned to available
insufficient_funds a payout larger than available

simulate goes through the same path as a real provider notification (inbox → state machine → ledger → webhook), so what you see in test mode is what live mode does. It returns 403 with a live key.