Sandbox
Audience: anyone integrating Mooov — platforms on Mooov Connect and merchants using a direct API key. Build and certify against the sandbox before you touch production.
The sandbox is a full deployment of the Mooov gateway wired to PSP test mode. Same API surface, same HMAC auth, same webhook envelopes, same state machine — no real money moves.
Building with an AI agent or MCP-capable editor? It can pull these docs and schemas itself; see Connect your AI agent.
1. Base URLs
API https://sandbox.api.mooov.money
Authorize https://sandbox.connect.mooov.money/authorize (Connect only)
Production https://api.mooov.money
(The legacy staging.*.mooov.money hostnames remain permanent
aliases for the same sandbox; existing integrations keep working.)
Sandbox and production are completely separate: separate keys, separate merchants, separate grants, separate webhook destinations. Nothing you create in the sandbox exists in production. The full hostname matrix (portals, docs, MCP) is on the environments page.
2. Test credentials
The environment is baked into your API key at minting time, and a
key only works against its own environment's hostname (use mk_test_*
keys with sandbox.api.mooov.money). Test keys are visually distinct
so a leaked-into-prod-config mistake is greppable:
| Credential | Sandbox prefix | Production prefix |
|---|---|---|
| Merchant API key | mk_test_ |
mk_live_ |
| Platform (Connect) key | mk_platform_ (Mooov-Test-Mode key) |
mk_platform_ (live key) |
- Merchants mint test keys from the merchant portal
(
mooov3.mooov.money→ Developers → API keys). The secret is shown once at creation; store it in your secret manager. - Platforms receive a Mooov-Test-Mode platform key during the credential ceremony described in §9 of the Connect protocol. Sandbox and prod platform keys are different secrets and never share grants.
Signing is identical in both environments — the four-line canonical string and headers from §2 of the Connect protocol.
3. Test cards
Which test cards work depends on how your charge routes. Mooov picks the acquiring route per merchant; you don't choose it per request. Most merchants route to Stripe test mode (§3.1). Merchants approved for Mooov's high-risk acquiring route (you'll know — it involves an eligibility review) route to the high-risk sandbox (§3.2).
3.1 Stripe-routed merchants
The standard Stripe test PANs drive every outcome you need to handle:
| Card number | Result |
|---|---|
4242 4242 4242 4242 |
Approved immediately |
4000 0025 0000 3155 |
Requires 3DS authentication (challenge flow) |
4000 0000 0000 9995 |
Declined — insufficient_funds |
4000 0000 0000 0002 |
Declined — generic card_declined |
4000 0000 0000 0341 |
Attaches for future use, then fails when charged off-session |
Use any future expiry date, any 3-digit CVC, and any postcode. On a
hosted checkout (flow: "redirect" / flow: "embedded") you type
these into the Stripe-hosted form; for server flow you tokenise them
first via a test-mode Setup Intent or Payment Element.
4000 0000 0000 0341 is the important one for saved-method flows:
it lets you exercise the payment.failed webhook path for
POST /v1/charges/saved without manufacturing a real decline.
3.2 High-risk-routed merchants (high-risk sandbox)
Three things are different from Stripe before you reach for a card:
- HKD only. The high-risk sandbox accepts
currency: "HKD"exclusively. A test charge in GBP/EUR/USD will be rejected at creation. (Production supports more currencies; the sandbox does not.) - Hosted checkout only. Use
flow: "redirect"and send the customer tohosted_url. Card entry happens on the acquirer-hosted page; there is no test-token server flow. - Some cards require an exact amount to trigger their 3DS scenario — see the table.
| Brand | Card number | Scenario | Notes |
|---|---|---|---|
| JCB | 3566 1111 1111 1113 |
Approved, no 3DS | The dedicated non-3DS card — use this first |
| Mastercard | 5555 5555 5555 4444 |
Usually approves without 3DS | Can be pushed into authentication; if it fails, use the JCB above |
| JCB | 3566 0000 0000 0001 |
3DS frictionless | Auto-passes, no challenge UI |
| JCB | 3566 0000 0000 0007 |
3DS challenge | Shows a challenge page |
| Mastercard | 5186 0076 0009 0864 |
3DS frictionless | Amount must be exactly HKD 2.01 (amount: 201) |
| Mastercard | 5506 9001 4010 0305 |
3DS challenge | Amount must be exactly HKD 20.90 (amount: 2090) |
| Visa | 4012 0000 3333 0026 |
3DS challenge | Amount must be exactly HKD 2.09 (amount: 209) |
Any future expiry and any 3-digit CVV work for all of the above. If the hosted form asks for billing address fields, any plausible values are accepted.
Refunds work the same as Stripe-routed merchants — full and partial
via POST /v1/payment_intents/{id}/refund — with one provider rule
that also applies in production: the refund window is six months from
capture, and the refund currency must match the charge currency.
4. Sandbox tenants (Connect)
Sandbox merchants seed automatically when you exchange an auth code:
run the consent flow from
§3 of the Connect protocol against
sandbox.connect.mooov.money/authorize and the resulting
merch_test_* merchant is ready to charge immediately. A fresh CI
run can spin up tenants without any manual seeding on Mooov's side.
If a sandbox tenant has not finished simulated PSP onboarding you
get the same typed 422 merchant_not_charge_capable you'd see in
production — see the error reference —
so you can build your onboarding-incomplete UX against the sandbox
too.
5. Webhooks in the sandbox
Register a sandbox webhook destination separately from production
(platforms: part of the credential ceremony; merchants: portal →
Developers → Webhooks). Deliveries are signed with your sandbox
webhook secret using the same t=<unix>,v1=<hex> scheme documented
in §6 of the Connect protocol.
Two practical ways to trigger events:
- Drive a real test payment. Complete a
4242…checkout and you'll receivepayment.succeededwithin a couple of seconds; refund it viaPOST /v1/payment_intents/{id}/refundto receivepayment.refunded. This exercises the full pipeline end to end and is what we recommend for CI. - Decline paths. Use the decline PANs above to receive
payment.failedwith a populatedfailure_code/failure_category.
The full event list and payload shapes are in the webhook event catalog.
Webhook retries behave exactly as in production (exponential backoff:
1s, 5s, 30s, 5m, 30m, 2h, 12h), so you can verify your dedupe logic
by returning a 500 from your sandbox endpoint and watching the
redeliveries arrive with the same X-Mooov-Delivery semantics.
6. Go-live checklist
Before requesting production credentials, confirm in the sandbox:
- Signature verification rejects a tampered body and a stale timestamp (>5 min skew).
- Idempotent retries: replaying a
POSTwith the sameIdempotency-Keyand body returns the original result; a reused key with a different body returns 409. -
payment.succeeded,payment.failed, andpayment.refundedhandlers are idempotent onevent.id. - The
422 merchant_not_charge_capablepath surfaces thesetup_urlto the merchant instead of erroring blindly. - (Connect) Grant revocation: after
POST /v1/grants/{id}/revokeyour system marks the tenant disconnected on the next 403GRANT_NOT_FOUND.
Then follow §9 of the Connect protocol (platforms)
or mint a live key from the portal (merchants) and switch the base
URL to https://api.mooov.money.