Skip to content
NOSTREL
Sandbox

Making it work is
the easy half.
Make it fail.

Every sandbox page explains how to produce a successful payment, which is the one case nobody needs help with. Six failures below, each with how to trigger it, what should happen, and what to check in your own code.

Work through all six before you go live. It takes under an hour and it is the difference between an incident and a Tuesday.

01Not a mock

It runs against Safaricom's own sandbox

Which sounds like a detail and is the whole value.

A mock reproduces the failure modes its author thought of. That is a useful thing to have and it is, by definition, missing exactly the cases that catch people out, because nobody writes a mock for a possibility that has not occurred to them.

Safaricom's sandbox produces the real shapes: the actual result codes, the actual timing, the actual difference between a decline and a timeout, and the genuinely strange edges that only exist because a real system is on the other end. Our own validation ran against it, and several things we had assumed turned out to be wrong, which is the point.

The cost of that honesty is that the sandbox is occasionally slow or unavailable, exactly as the production rail occasionally is. We prefer that to a mock that is always up and always lying.

02Six drills

Rehearse these, in this order

Roughly by how soon you will meet them, which is close to the reverse of how likely you are to have tested them.

  1. Drill 01Constantly

    The payer cancels

    By far the most common non-success. Somebody opens the prompt, thinks better of it and dismisses it. Raise a prompt on a sandbox number and simply decline it.

    What should happen

    The collection reaches failed with the provider result code attached. No fee, no ledger movement.

    Check in your own code

    Your order does not move to paid, and your UI says something a customer can act on rather than "error".

  2. Drill 02Daily

    Nobody answers

    The phone is in a bag, the payer is driving, the handset is off. Raise a prompt and ignore it until the window closes.

    What should happen

    The collection reaches timed_out, which is deliberately not the same as failed, because this one is worth offering again.

    Check in your own code

    You offer a retry rather than telling the customer their payment was declined. Those are different sentences and customers notice.

  3. Drill 03Weekly

    Not enough balance

    Request an amount larger than the sandbox account holds. This is the one that produces the most confusing support conversations, because the customer is certain they paid.

    What should happen

    failed, with the provider result code that says insufficient funds.

    Check in your own code

    Your message distinguishes "we could not take the money" from "something broke". Only one of those is the customer's to fix.

  4. Drill 04Rare, and expensive

    The callback never arrives

    Point your endpoint at a URL that times out, or simply turn your server off mid-payment. This is the scenario that separates an integration that survives from one that does not.

    What should happen

    The payment resolves anyway. A sweeper queries the provider for the real outcome rather than waiting for a message that is not coming.

    Check in your own code

    When your server comes back, you reconcile from the collection record rather than assuming nothing happened while you were down.

  5. Drill 05Whenever a network hiccups

    Your client retries

    Send the same request twice with the same Idempotency-Key, then once more with the same key and a different amount.

    What should happen

    The first replay returns the first response. The one with a changed body returns 409 with idempotency_key_reuse.

    Check in your own code

    Your retry logic reuses the key rather than generating a fresh one. A new key per attempt is a new payment per attempt.

  6. Drill 06The day you are found

    Somebody forges a webhook

    POST a plausible collection.succeeded to your own endpoint with no signature, then with a signature from the wrong secret, then with a real signature and a timestamp an hour old.

    What should happen

    All three rejected, with no record touched.

    Check in your own code

    This is the one people skip. An endpoint that accepts any of those three is an endpoint that will eventually credit an order nobody paid for.

03Drill six, expanded

Attack your own webhook endpoint

It is a public URL that credits orders. Somebody will eventually try these three. Better that it is you, today, with nothing at stake.

three requests that must all be refusedbash
# Drill six, the one people skip. All three of these must be refused. # 1. No signature at all.curl -X POST https://your-server.test/hooks/nostrel \  -H "Content-Type: application/json" \  -d '{"type":"collection.succeeded","data":{"amount":500000}}' # 2. A real signature, from the wrong secret.#    Passes? You are comparing against#    something you hardcoded. # 3. A genuine past delivery, replayed later.#    Passes? You are not checking the age.

04Before you switch

What changes when you go live

Less than you would expect, which is deliberate: a sandbox that behaves differently from production is a sandbox that teaches you the wrong lessons.

The key, and only the key

A live key instead of a test key. Same endpoints, same shapes, same state machine, same event names.

Real money and real people

Which means the timing changes. Sandbox payers answer instantly because they are you; real ones take as long as a person takes.

Verification has to be done

Collection is gated on approval. You can build and test everything before that, and you cannot take a real payment until it is complete.

Limits apply

Per-transaction and monthly ceilings by tier. Visible in the console, and worth reading before a launch rather than during one.

Found something we did not list?

A failure mode we have not written down is a gap in this page and probably a gap in the product. Tell us what you did and what happened, and it goes on the list.