Receive money in Kenya
Shillings arriving at the collection account we issue you: which rails reach it, what attributes each payment to an order, and why one rail gives you no reference to match on.
Shillings reach you at a collection account issued to your partner account. Four Kenyan rails can reach it, and they divide into two groups that behave differently enough to change how you build attribution.
This page is about money coming in. Money going out settles to an account registered against your partner account in advance — see KES.
Where the money goes
GET /v1/funding is the source of truth for your instructions, and the create
response on a buy order repeats them in paymentInstructions. Read them from
the response rather than storing a copy: the account we publish is yours, but
it is ours to change.
{
"kes": [
{
"rail": "mpesa_paybill",
"paybill": "000000",
"accountReference": "00000000123456",
"currency": "KES",
"instructions": "M-PESA > Lipa na M-PESA > Pay Bill > Business number 000000 > Account number 00000000123456."
},
{
"rail": "bank_transfer",
"accountReference": "00000000123456",
"beneficiaryName": "OnLink Technology Inc",
"currency": "KES",
"instructions": "Send KES to account 00000000123456 by PesaLink, RTGS or EFT. Put the paymentReference from your order in the narration."
}
]
}One entry per rail family available to you — mpesa_paybill, and
bank_transfer, which covers PesaLink, RTGS and EFT — and the array can be
empty. A partner whose collection account is not activated yet gets []
rather than a placeholder, because publishing an account nothing is watching
is money arriving nowhere. The usdt half of the same response is the sell
leg, and USDT on Tron covers it.
accountReference is the exact value the payer must quote, verbatim. On
the Pay Bill rail it goes in M-PESA's Account Number field; on a bank transfer
it is the beneficiary account number. It is your own dedicated collection
account, which is what makes matching inside it safe.
beneficiaryName is the name a bank transfer must be addressed to. It is
present only when our bank has confirmed the name back to us. If it is absent,
ask us rather than composing one: the receiving bank matches on that string,
and a guess decides whether the money lands.
Choose the rail on the order
POST /v1/orders/buy requires paymentRail, one of mpesa, pesalink,
rtgs or eft. It is not cosmetic: the rate guarantee is per rail, attribution
differs by rail, and the paymentInstructions on the create response are for
the rail you declared. So the rail is chosen by you, when you create the order,
and your payer pays on it — not on whichever rail they happen to prefer.
The two vocabularies line up like this: mpesa is the mpesa_paybill entry
above, and pesalink, rtgs and eft are all the bank_transfer entry.
The two groups, and the difference that matters
| Rail | What the payer does | What attributes the payment |
|---|---|---|
| M-PESA Pay Bill | Pays the Pay Bill, quoting your account number | The account number, plus the exact amount inside the window |
| Bank transfer (PesaLink, RTGS, EFT) | Transfers to the account number we publish | The paymentReference we issue, carried in the narration |
On a bank transfer the reference we mint travels with the money and attributes the payment exactly, whatever the amount. On M-PESA it does not travel at all.
On M-PESA there is no reference to match on
M-PESA Pay Bill has no narration field. There is nowhere to put the
paymentReference we issued, so nothing we mint can ride along with the
payment — and there is no version of your integration that changes that.
What identifies the payment instead is the account number, which is yours and is the same on every payment, plus the amount and the time window. That is enough to attribute a payment to your account, and only sometimes enough to attribute it to one order.
Two of your open orders carrying the same kesAmount inside the same window
are genuinely ambiguous. We will not guess: both are held rather than one being
picked. Use a bank transfer for same-amount orders, or do not hold two
identical open orders at once.
If you need to attribute a payment to one of your own users
This is the part worth designing before you build, because the handle you get is not the one most integrations assume.
- On a bank transfer, you have the reference we minted. It is ours, we validate it, and it identifies exactly one order.
- On M-PESA, nothing the payer types reaches you. The handles are the
account number, which is the same on every payment; the amount; the window;
and, once we have attributed the payment, the
providerReference— the M-PESA receipt code the payer also holds. That code is the one thing a payer can quote back to you.
If your product asks contributors, customers or members to identify themselves in a payment, do not build the design that assumes an identifier arrives with the money. On M-PESA it cannot. Build the one that gives each expected payment its own order, reconciles on amount and time, and has a path for a payment you cannot place. On the rails where a reference does travel, use it and prefer them.
A credit we cannot attribute to any order is not lost. It is in your collection
account, and GET /v1/balances reflects it — but
nothing on this API moves it today, so contact us with the amount and the time
rather than waiting for a webhook that will not come.
The whole leg, end to end
sequenceDiagram
autonumber
accTitle: Shillings arriving, from your payer to your webhook
accDescr: Seven messages between your payer, the Kenyan rail, OnLink and you. You create a buy order, declaring the rail your payer will use, and OnLink returns payment instructions for that rail. You show your payer the account number and, on a bank transfer, the reference. Your payer pays on that rail. The rail credits the account OnLink issued you. OnLink attributes the credit to your order. OnLink posts you the order.funds_confirmed webhook.
participant Payer as Your payer
participant Rail as A Kenyan rail
participant OnLink
participant You
You->>OnLink: create a buy order, declaring paymentRail
OnLink-->>You: paymentInstructions
You->>Payer: show the account number, and the reference on a bank transfer
Payer->>Rail: pay on that rail
Rail->>OnLink: credit the account we issued you
OnLink->>OnLink: attribute the credit to your order
OnLink-->>You: webhook order.funds_confirmedIn order:
- You create the buy order, declaring
paymentRail, and readpaymentInstructionsfor that rail off the response. - You show your payer the account number — and, on a bank transfer, the reference we issued.
- Your payer pays, on the rail you declared.
- The rail credits the account we issued you.
- We attribute the credit to your order, by the reference on a bank transfer or by the amount and the window on M-PESA.
- We post you
order.funds_confirmed.
What tells you the money arrived
order.funds_confirmed — a signed webhook fired once we have
attributed the payment to an order. Until that fires we cannot tell "has not
arrived" from "arrived, and we do not know which order it is for", and those
are different problems with different remedies.
Polling GET /v1/orders/{id} also works and
terminates. Use the webhook as the primary signal and the poll as the fallback.
Once a payment is attributed, the order carries the rail's own code alongside ours:
providerReference— the M-PESA code from your payer's handset, or their bank's reference. This is the one to show your end user; ours means nothing to them.paymentReference— ours, for your reconciliation against us.
Both are null until a payment is attributed, and both are opaque strings:
store them, show them, do not parse them.
Before you build
- Declare the rail your payer will actually use.
paymentRailis fixed at creation, and the instructions you get back are for that rail alone. - Pay the exact amount, before the deadline. The deadline is
expiresAton the order. An order that is never funded expires; a funded one does not. - One payment funds one order. A single payment meant to cover two orders does not apply its surplus to the second.
- Prefer a bank transfer where you have the choice. Exact attribution regardless of the amount is worth more than the convenience of the other rail, and it is the difference between reconciling and guessing.
- Read the cut-offs. M-PESA and PesaLink run continuously; RTGS and EFT observe a banking day. See Cut-off times.