Testing

Test mode is the same API, the same hosted checkout and the same webhooks on a second book that never holds money. A test key reaches it; a live key never does. This page says what is shared, what is separate, what you can make happen and what is not yet available.

Test keys and live keys

A key starts with sk_test_ or sk_live_, and that prefix decides the mode of every object the request creates or reads. There is no mode parameter on /v1 and no way to reach the other book with the wrong key: a test key listing payment intents sees test intents only, and a live key sees live ones only.

The dashboard works the same way. https://dash.lemak.cash/test is every screen in test mode, under a banner that says so; the same path without the prefix is live. Keys are created under Developers with the mode you choose, and both lists are shown together, each row labelled.

Per modeShared by both modes
Payment intents, attempts, disputes, settlements and events. Balances: the mode is part of every ledger account's key, so test money cannot land on a live balance. Webhook endpoints: a test event reaches a test endpoint only, and carries livemode: false. Payout destinations: a test destination is always of type simulated. Rate limits: each key has its own bucket. Idempotency: the mode is part of the request digest, so a key reused across modes is 422 idempotency_key_reused. Your account, its users and roles, the IP allow-list, the request signature scheme, your pinned API version, and your pricing: a test payment shows the real fee and the real spread, which is the point of testing.

What works before activation

Our operators create your account and set it active; there is no self-service sign-up. Once the account is active, a test key and a live key both work, and no further review gate separates them today. Activation in the Stripe sense, where test mode opens at once and live payments and settlement open only after a review, is planned and not yet available. Until then, treat a live key as live: it takes real payments on a real channel.

Live payments go to a production channel. Test payments go to a simulator channel that our operators configure for your account; a test request that no channel serves answers 402 no_channel_available.

The simulate route

The test channel does nothing on its own. A test payment stays at requires_payment with a QR code nobody can scan until you tell the channel what to report, with POST /v1/test/payment_intents/{id}/simulate or the Simulate button on a test payment in the dashboard.

curl https://api.lemak.cash/v1/test/payment_intents/pi_AGQHAVEX7J327MRWOPILG46G6Y/simulate \
  -H "Authorization: Bearer sk_test_MFWZLFDEM5X3WDBPM7ULRVDKCE" \
  -H "Idempotency-Key: Q2WJN4QF6XKZTA3Y7BRM5LDCVE" \
  -H "Content-Type: application/json" \
  -d '{ "outcome": "succeeded" }'

The intent must hold a pending attempt, which it does once the payer has pressed pay on the hosted page or you created it with a payment_method. The simulator then emits the callback a real channel would send, and that callback runs through the real path: stored, signature checked, picked up by a worker within a few seconds, confirmed by a query back to the channel. The response is the intent as it stood when the callback landed; the result follows by webhook or on GET /v1/payment_intents/{id}.

BodyWhat happens
{ "outcome": "succeeded" }The callback reports success, the confirming query agrees, and the intent moves to succeeded. Event: payment_intent.succeeded.
{ "outcome": "failed" }The attempt is marked failed and the intent moves to failed. Event: payment_intent.failed.
{ "outcome": "pending" }The callback arrives but the query still says pending, so the intent moves to processing and waits. A later simulate with succeeded or failed finishes it. Event: payment_intent.processing.
{ "outcome": "succeeded", "amount": 100001 }The callback reports an amount other than the one ordered. The callback amount alone never makes a payment succeed: the intent moves to processing and an exception is filed for our reconciliation. It does not reach succeeded. currency can be changed the same way.

Refusals: a live key answers 400 test_mode_required; an intent whose channel cannot be told what to report answers 400 simulation_unsupported; an intent with no pending attempt, or another merchant's, answers 404 resource_missing.

The hosted checkout in test mode

A test intent's checkout_url opens the same page a payer sees in live mode. The credential shown is the simulator's, a QR code with data of the form mock://pay/…. Pick a method, press pay, then drive the outcome with the simulate route; the page polls, sees succeeded and redirects to your return_url. A button on the page itself for choosing the outcome is not yet available.

Webhooks in test mode

Register the endpoint on the test surface of the dashboard; it receives test events only, every one with livemode: false. The signature, the retry schedule and the delivery log are identical to live. See Webhooks.

Magic amounts

Not yet available. A later release will let the last digits of a test amount choose the outcome, as Stripe's test amounts do. Today no amount changes what the simulator does: every test payment waits for the simulate route, whatever its amount. This page will list the values when they ship; do not build against any value you have seen elsewhere.

Not yet available

  • Decline reasons on a simulated failure: the route takes outcome, amount and currency and nothing else.
  • Simulating a dispute, or a dispute's outcome.
  • Moving time: letting a test intent expire or its funds release on demand.
  • Deleting or resetting your test data.
  • An outcome panel on the hosted checkout page.
  • Client libraries. Every example in these guides is curl or plain fetch.