Skip to content
NOSTREL
Collections

Five ways to get paid.
One place they all land.

A till payment, a paybill payment, an STK prompt, a link sent on WhatsApp and an invoice are five different conversations with Safaricom. They become one row in one ledger, with one shape of event, before they reach you.

Which matters more than it sounds. The businesses that struggle with M-Pesa are rarely the ones that cannot take a payment. They are the ones taking payments four different ways and reconciling the four by hand on the last Friday of the month.

one prompt, one callbash
curl https://api.nostrel.com/v1/api/collections \  -H "Authorization: Bearer sk_live_••••••••" \  -H "Idempotency-Key: order-1042" \  -d phone=254712345678 \  -d amount=1850 \  -d reference=SC-1042
and then, when the payer is done
what we send youhttp
POST https://your-server.test/hooks/nostrelNOSTREL-Signature: t=1790000000,v1=5f2a...9cContent-Type: application/json

01The rails

Pick by how much work it is, not by how clever it sounds

Four of these five need no engineering at all. If you are a shop rather than a software company, start at the top of that list and stop there.

STK push

Lipa na M-Pesa Online

You send an amount and a phone number. A prompt appears on that handset, the payer enters their PIN, and you get a webhook. The payer never types a business number or a reference, which is where most failed payments come from.

One API call

Anything where your software already knows who is paying and how much.

Payment link

A URL you send

Create a link in the dashboard with an amount and a description, or leave the amount open. Send it on WhatsApp. The payer opens a checkout page, types their number, and answers the prompt. Every payment lands in the same ledger as the API ones.

No code at all

Invoicing by message, and every business that will never write an integration.

Invoice

Line items and a due date

A link with a document attached to it: line items, totals, a due date and a hosted page the customer can pay from. It moves from draft to sent to paid, and the payment is a collection like any other.

No code at all

Business to business, where somebody needs a document for their own books.

Paybill

C2B, account-reference routed

Your customers pay a paybill number and type an account reference. We match the reference to the right record and raise the same event. Use the platform shortcode, or bring your own and we push from yours.

Configure once

Customers who already know your paybill, and anyone who prints it on a receipt.

Till

Buy Goods

The counter case. No account reference to mistype, which is exactly why it suits a queue, and exactly why reconciling it by hand is miserable without something recording each payment against a sale.

Configure once

Shops, restaurants, anywhere a person is standing in front of you.

02State

Seven states, and one of them is the whole argument

This is the table your integration is really written against, so it is on the marketing page rather than three clicks into a reference.

  1. initiated

    We have accepted your request and are asking Safaricom to raise a prompt. Nothing has reached a handset yet.

    nothing movedpending, failed
  2. pending

    The prompt is on the payer's phone. Now it depends on a person finding their handset, which is the slowest part of any payment and the part you cannot engineer around.

    nothing movedawaiting_confirmation, failed, timed_out
  3. awaiting_confirmation

    A callback says this succeeded, and we have not yet heard Safaricom say so in an exchange we started. Your balance has not moved. Most platforms do not have this state, which is why most platforms can be made to credit a payment that never happened.

    nothing movedsuccess, failed
  4. success

    Confirmed independently, posted to the ledger, fee taken, webhook sent. Safe to ship the goods. This is a terminal state.

    balance changedfinal
  5. failed

    The payer cancelled, got the PIN wrong, had no balance, or Safaricom refused it. The provider result code and its description both come back on the record, so your retry logic can read a number rather than pattern-match English. This is a terminal state.

    nothing movedfinal
  6. timed_out

    Nobody answered the prompt and the window closed. Distinct from failed on purpose: this one is worth retrying, and a failure usually is not. This is a terminal state.

    nothing movedfinal
  7. reversed

    A confirmed payment was later reversed at the provider. The ledger posts the reversal rather than editing the original, so the history stays true. This is a terminal state.

    funds releasedfinal
Every state a collection can hold. A filled square is terminal: once a record is there it will not change again, which is what makes it safe to act on. A hollow one means something is still owed, by Safaricom, by the payer, or by us.

03The event

The same shape, whichever rail it came in on

A till payment and an API payment differ by one field. Everything your code branches on is identical, which is the entire point of putting five rails behind one ledger.

  • Amounts are integers

    Minor units, always. No float ever touches a money column, from the request through the ledger to the statement.

  • The fee is on the event

    Gross, fee and net, at the time it happened. Not derivable later from a rate card that has changed since.

  • The payer is masked

    You get enough to recognise a customer and not enough to be a liability if your logs leak.

  • The provider receipt is included

    The code the payer sees on their own phone, so a support conversation can start from the same string the customer is reading out.

collection.succeededjson
{  "id": "evt_8sk2mq04nf",  "type": "collection.succeeded",  "created_at": "2026-10-02T09:14:22Z",  "data": {    "id": "col_7dj4a2zeialj",    "state": "success",    "amount": 1850,    "currency": "KES",    "fee": 25,    "net": 1825,    "reference": "SC-1042",    "channel": "stk_push",    "payer_masked": "2547XXXXX678",    "provider_receipt": "SKL4H8T2QD"  }}

Signed with HMAC over exactly these bytes. The webhooks page works through verifying it, including the mistake nearly everyone makes once.

04Where it lands

Collections and payouts in one list, because two lists is how the discrepancy starts

Search by reference, receipt, phone or id. Filter by rail, by state, by date. Export the lot as CSV when your accountant asks, which they will.

app.nostrel.com/transactions
The NOSTREL transactions list, showing collections and payouts together with status, counterparty, reference and amount.
Every collection and every payout in one ledger view. Demonstration data from a development environment.

05Not your problem

The parts nobody budgets for

Every one of these is a week somebody has lost to it, usually after launch, usually in public.

The double charge

Your HTTP client timed out and retried. The payer got two prompts. An Idempotency-Key is required here, so the second call returns the first result instead of taking more money.

The lost callback

Safaricom sent it, your server was restarting, it is gone. A scheduled sweeper asks the provider for the real outcome rather than waiting for a message that is not coming.

The month-end reconciliation

A daily job matches our ledger against the provider record for the same window and opens an exception for anything that does not line up, with a state so a person closes it.

The fee arithmetic

Percentage, fixed part, floor, ceiling, dated so a change is not retroactive. Computed once, posted to the ledger, visible on the transaction and in the statement.

The statement

Opening balance, in, out, fees, closing balance, for any date range, drawn from the same postings as the transaction list rather than computed a second way.

The 2am question

Which record do I believe. There is one ledger, it balances, and every movement has two sides. That is the answer and it is the reason the rest of this exists.

Try it against the real sandbox

Not a mock we wrote. Safaricom's own, where a cancelled prompt and an insufficient balance behave the way they will on the day it matters.