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.
Create a sandbox account from code
Section titled “Create a sandbox account from code”No human needed: the sandbox API opens a test-only account and returns its keys once.
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 returns409 secret_unavailable(withdetails.merchant_id) — the secrets are never shown twice. - Limits: 5 signups per hour and 20 per day per address (
429 rate_limited, honourRetry-After). At most 3 sandbox accounts per email, for good: the fourth answers422 limit_exceeded— retrying won’t help, use an existing account. webhook_urlis optional and must behttps://. Production (api.xaalis.org) doesn’t have this route (404).- AI agents do the same with the MCP tool
xaalis_sandbox_signup(AI agents).
Simulating outcomes
Section titled “Simulating outcomes”| 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.