The Push Is Not the Payment
The response to a payment request is not a payment. It is two identifiers and a suggestion that you ask the customer to enter their PIN. Everything that actually matters arrives later, on a different connection, from a party that signs nothing.
A card payment can be synchronous: you ask, you wait a moment, you are told. A mobile money payment cannot, because in the middle of it is a person holding a phone, and people take as long as they take. That single fact reshapes everything downstream — what a response means, where the truth lands, and what your system has to do while nothing is happening.
What the immediate reply actually contains
Send a payment request and, a second or two later, you get back two identifiers and a sentence. The identifiers name the request. The sentence says to ask the customer to enter their PIN.
That is the whole of it. No amount confirmed, no receipt, no outcome — because at the instant of the reply the customer has not looked at their phone yet.
It is a receipt for having asked. The answer is a separate event, and it arrives when a person decides.
So the payment record in your workspace starts as pending, carrying those identifiers, and waits. A system that treated the reply as a payment would be marking sales as paid at the moment somebody's phone buzzed.
And the reply can refuse without saying so
One more thing about that first response, and it is the third time this pattern has appeared in this series.
A perfectly healthy response can carry a refusal inside it. There is a result code in the body, and only one value means the request was accepted. Anything else — a malformed number, a short code that cannot receive this transaction type, a limit — arrives with an ordinary successful status.
So acceptance requires both: a successful response and that specific code. A connector testing only the status would create pending payments for requests that were never sent, and they would sit there forever, because nothing was ever going to arrive for them.
The failure is a payment that stays pending rather than an error, which is exactly the kind that survives a long time, because a pending payment looks like a customer who has not paid yet.
Where the answer comes from
The outcome arrives as a separate request from the provider to an address supplied when the payment was requested. That is where the money is confirmed, and it is a different kind of connection with a different security problem.
This provider does not sign its callbacks. There is no shared secret, no signature to verify — which means the only thing distinguishing a genuine confirmation from an invented one is where it came from.
So the source address is checked against the provider's published ranges, and that check fails closed: with no list configured, nothing is accepted. That is deliberately the opposite of the arrangement used for a card provider elsewhere in the product, and the difference is worth explaining because it looks inconsistent.
| This provider | A signing provider | |
|---|---|---|
| Signs its callbacks | No | Yes, and it is verified |
| Address filtering is | The entire authentication | A second lock |
| With no list configured | Nothing is accepted | Everything is accepted |
| Why | There is nothing else | The signature already decided |
The reasoning is one rule rather than two: a check may fail open only where something else authenticates. Where it is the only check, on an endpoint that is public and moves money, clearing the list must not switch the lock off.
When the answer never comes
Callbacks are a promise from a third party over the internet, and promises like that are kept most of the time.
For the rest, there is a query: hand back the identifier you were given and ask what happened. It is the same computed credential as the payment request, against a different endpoint, and it returns a result code and a description.
One detail of its shape is worth pointing at, because it is a distinction most code collapses. Whether the query succeeded and what the payment did are two separate fields. A query that could not reach the provider is a different fact from a payment that was cancelled, and merging them into one boolean throws away which of the two you are looking at — leaving you unable to tell a network problem from a customer who pressed cancel.
Three ways to be told, in one connector
It is worth collecting these, because a payment integration that offers several flows offers several confirmation models, and they are not interchangeable.
-
The push, confirmed by callback
The customer is prompted on their phone. The outcome arrives as a separate request, and a query exists for when it does not.
-
The scannable code, confirmed out of band
The customer scans and pays. That endpoint has no callback at all — the funds simply land in the till, and reconciliation is against the account rather than against an event.
-
The reversal, confirmed by callback
Also asynchronous, and needing stronger credentials again, because sending money back is a different privilege from asking for it.
The middle one is the one to be honest about. A payment code produces money in your account and no event in your system, so a sale paid that way is reconciled by looking at the account. That is not a shortcoming of the connector — it is what that endpoint offers — and knowing it before you choose that flow is the difference between a workable process and a monthly surprise.
What is in place, layer by layer
From request to confirmed payment
The reply treated as an acknowledgement
Two identifiers and a prompt, recorded against a pending payment rather than mistaken for an outcome.
Acceptance judged on the body, not the status
A healthy response carrying a non-zero result code is a refusal, and treating it as accepted would create payments that could never resolve.
The provider's own refusal text surfaced
A rejected request reports the message the provider wrote, which names the number, the short code or the limit.
A callback address supplied per request
The outcome arrives at an address given with the request rather than at a fixed one, so the confirmation is tied to the flow that raised it.
Source filtering that fails closed
This provider signs nothing, so the source address is the entire authentication — and clearing the list refuses everything rather than accepting anything.
A query for the silent cases
The identifier can be handed back to ask what happened, using the same computed credential against a different endpoint.
Query outcome and payment outcome kept apart
Whether the question was answered and what the answer was are separate fields, so a network failure is never read as a cancelled payment.
Reversals as their own privilege
Sending money back is asynchronous too and requires stronger credentials, because it is a different act from asking for money.
A bounded wait on every call
No payment request holds a checkout open indefinitely waiting for a provider to answer.
The scannable payment code has no callback at all. Funds land in the till and reconciliation is against the account rather than against an event — which is what that endpoint offers, and worth knowing before choosing that flow.
Three positions held on purpose
- A payment is pending until something confirms it. The acknowledgement is recorded as an acknowledgement, because marking a sale paid when a phone buzzed is a reconciliation problem that compounds daily.
- Source filtering fails closed on the provider that signs nothing, and open on the one that does. It is one rule rather than two: a check may fail open only where something else authenticates.
- Whether a query worked and what it found are separate answers. Collapsing them means a connection problem and a cancelled payment become the same fact, and no amount of care further up can separate them again.
Five questions about a mobile money integration
What does the immediate response mean?
A good answer sounds like
The request was accepted.
What ours actually is
Two identifiers and a prompt. The payment is pending until a confirmation arrives.
Can a successful response be a refusal?
A good answer sounds like
Yes — they check the body.
What ours actually is
Acceptance needs both a successful status and the specific code that means accepted.
How is the callback authenticated?
A good answer sounds like
By source, and it fails closed.
What ours actually is
This provider signs nothing, so the address check is the whole authentication and clearing it refuses everything.
What if the confirmation never arrives?
A good answer sounds like
There is a query.
What ours actually is
The identifier is handed back to ask what happened, with the query result and the payment result kept separate.
Do all the payment flows confirm the same way?
A good answer sounds like
No, honestly.
What ours actually is
The push confirms by callback; the scannable code has none and is reconciled against the account.
Our take
The hardest part of accepting mobile money is not the integration, it is that a person is in the middle of the transaction and your system has to be comfortable not knowing. Anything built on the assumption that a payment resolves within a request is going to be wrong in a way that shows up as reconciliation work rather than as an error — sales marked paid that were not, sales stuck pending that were, a monthly exercise nobody can automate. Getting it right is mostly a matter of accepting the shape: the request is a request, the answer is an event, and there has to be a way to ask when the event does not turn up.
A payment is pending until something says otherwise
The difference between a clean month-end and a reconciliation exercise is usually whether the system was willing to say it did not know yet.
Talk through M-Pesa collectionFrequently asked questions
Why does a payment stay pending after the customer says they paid?
Because the confirmation is a separate event from the provider and it has not arrived yet, or did not arrive at all. The identifier from the original request can be handed back to ask what happened, which is what the query path is for — and its answer distinguishes a payment that completed from one that was cancelled.
How do you know a payment confirmation is genuine?
By where it came from. This provider does not sign its callbacks, so the source address is the entire authentication, checked against their published ranges. That check fails closed: with no ranges configured, nothing is accepted, because switching off the only lock should not be a side effect of clearing a setting.
Can a customer pay by scanning a code instead?
Yes, and it confirms differently. That endpoint has no callback, so funds land in your till and there is no event in your workspace to match them to. Reconciliation for that flow is against the account rather than against a confirmation, which is worth knowing before choosing it.
Does the connector ever mark a sale paid on its own?
Only on a confirmation. The immediate response to a payment request is an acknowledgement that the request was accepted, and it is recorded as one. A sale moves to paid when something says the money moved.
Can we reverse a payment from the product?
A reversal is supported and is asynchronous like the payment itself, with its outcome arriving as a separate event. It also requires stronger credentials than a collection does, because returning money is a different privilege from requesting it.