PesaBridge
All insights
EngineeringJun 2026 · 9 min read

Prompt-to-pay, explained: STK push done right

How a merchant push turns into a PIN prompt on the customer's phone — and what it takes to make it instant, safe and idempotent.

T
The PesaBridge team · Engineering

Prompt-to-pay is the flow every mobile money customer knows in their hands without ever thinking about it. A merchant asks for money, a prompt appears on the customer's phone, they enter a PIN, and it is done. It is the most-used payment pattern in the markets that adopted mobile money early, and it feels effortless. That effortlessness is the product of a surprising amount of engineering working hard to be invisible. Get any of it wrong and the magic becomes a double-charge, a stuck payment, or a merchant who shipped goods for money that never settled.

This is what is actually happening behind the prompt, and what it takes to make it instant, safe and impossible to double-charge.

Two ways the prompt arrives

There are two mechanisms behind "a prompt appears," and they matter because they have different reach and different failure modes.

The classic mechanism is SIM Application Toolkit (STK) — code that runs on the SIM card itself, which is why it can push a secure PIN prompt to a basic feature phone with no app installed. STK is how the original mobile money systems reached everyone: the menu and the PIN entry are driven from the SIM, over the network, on hardware that has nothing else on it. The modern mechanism is an app push — a notification to a smartphone app that opens a confirmation screen. Both end in the same place: the customer authorises, and a charge moves through its lifecycle. A platform worth building supports both, because the customer base spans both.

The charge is a state machine

The single most important idea in prompt-to-pay is that a charge is not an event — it is a state machine with an explicit, well-defined set of states and transitions. Treating it as a fire-and-forget call is the root of most production incidents. The states are roughly:

  • Pending — the merchant has requested a charge; the prompt has been pushed; the system is waiting for the customer.
  • Authorised — the customer entered their PIN and confirmed; the system has checked balance and limits and is committing the move.
  • Completed — the ledger has posted balanced entries; the money has moved; the merchant can ship.
  • Declined / Expired / Failed — the customer refused, the prompt timed out, or a check failed; no money moved, and the charge is closed cleanly.

Every charge is in exactly one of these states at any moment, and it can only move along defined transitions. This is not bureaucracy — it is what makes the flow reasoned-about and recoverable. When something goes wrong, the question is always "what state is this charge in, and what transition stalled?" — a question with an answer, rather than a shrug.

How the merchant learns the outcome

The merchant's system needs to know whether the charge completed — to print a receipt, release goods, update an order. There are two ways to find out, and the difference between them is the difference between a sluggish integration and a crisp one.

  1. Polling. The merchant asks the status endpoint, repeatedly, "is it done yet?" It works, but it is wasteful and laggy: poll too often and you hammer the API; poll too rarely and the customer is standing at the till waiting.
  2. Webhooks. The platform pushes the merchant a signed callback the instant the charge changes state. The merchant learns within moments, makes no wasted calls, and gets a clean event to act on. This is the right primitive, and a well-built platform makes it the default with polling as a fallback.

What makes it trustworthy

Three properties turn a working demo into a system a merchant can build a business on.

Idempotency — the double-charge killer

Networks retry. If a merchant's "create charge" request times out and they resend it, you must not create a second charge. Every charge-creation request carries an idempotency key; the platform records it, and a repeat with the same key returns the original charge rather than starting a new one. The same discipline protects the confirmation step. The guarantee the customer feels — "I was only charged once" — is this property, enforced at the point money moves, not a hopeful check in the app.

Signed webhooks

A webhook is an unsolicited HTTP call arriving at the merchant's server claiming a payment completed. Why should they believe it? Because every callback is signed — an HMAC-SHA256 signature computed over the payload with a shared secret only the platform and merchant know. The merchant recomputes the signature and rejects anything that does not match, which makes forged "you've been paid" callbacks useless to an attacker. And because networks fail, webhooks are retried with backoff until acknowledged, so a momentary outage at the merchant does not lose the event — it is redelivered until their server returns success.

Expiry

An unconfirmed charge cannot linger forever. If the customer walks away without entering their PIN, the charge must expire cleanly rather than sitting as an open liability that might mysteriously complete an hour later. A bounded, predictable expiry window is what keeps the merchant's view of the world consistent with the customer's.

The edge cases that separate real systems from demos

The happy path — push, confirm, complete — is the easy quarter of the work. The interesting failures live in the gaps:

  • The customer confirms, but the network drops the acknowledgement. The charge is completed on the platform; the merchant never heard. The webhook retry exists for exactly this. The merchant's handler must be idempotent on its side too, so a redelivered "completed" event does not ship the goods twice.
  • Two confirmations race in. A retried confirmation must not double-post. The idempotency key and the state machine together ensure the second confirmation observes "already completed" instead of moving money again.
  • The charge expires at the same moment the customer confirms. The state machine must define a single winner — the transition is atomic, so the charge is either authorised or expired, never both, and the customer is told clearly which.
  • Reconciliation. At the end of the day, every completed charge must correspond to a balanced set of ledger entries and a settlement record. Because the charge lifecycle and the ledger are tied together — completion is the posting — this reconciles by construction rather than by investigation.

One lifecycle, every surface

The final piece of doing this right is that prompt-to-pay should not be a special case wired separately into the app, the till and the API. It should be one charge lifecycle that every surface drives: a customer scanning a merchant QR, a merchant pushing a charge from a POS, and a partner calling the developer API all create the same kind of charge, moving through the same states, posting to the same ledger, emitting the same signed webhooks. Build it once, correctly, and every channel inherits the guarantees.

Done this way, prompt-to-pay feels instant and is impossible to double-charge — not because nothing ever fails, but because every failure has a defined state, a retry, and a path back to consistency. That is what PesaBridge ships, on the apps and the developer API alike, against a single charge lifecycle.

Stored-value wallets Agent network Merchant payments USSD Developer API Prompt-to-pay KYC tiers Reversals Float distribution Settlement Signed webhooks White-label Stored-value wallets Agent network Merchant payments USSD Developer API Prompt-to-pay KYC tiers Reversals Float distribution Settlement Signed webhooks White-label

Ready to launch your wallet?

Book a demo and we'll stand up your brand, country and rails — and walk you through the apps, admin and API.