Receive money in Kenya
Two Kenyan rails credit your own KES balance — the same balance the buy leg debits directly. Neither rail attaches to an order, and the credit.received webhook is your only completion signal.
Shillings you receive in Kenya land on your own KES balance at OnLink — the same balance buying USDT with KES debits directly. This page is about topping that balance up. There is no per-order payment step on the buy leg any more: you create a buy order and we debit your balance the instant it is created, so nothing here is "for" any one order.
Where the money goes
GET /v1/funding is the source of truth for your deposit instructions. 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. This tops up your KES balance — it is not tied to any single order, and there is no reference to quote per order."
},
{
"rail": "bank_transfer",
"accountReference": "00000000123456",
"beneficiaryName": "EXAMPLE PAYMENTS LTD",
"currency": "KES",
"instructions": "Send KES to account 00000000123456 by PesaLink, RTGS or EFT. This tops up your KES balance — it is not tied to any single order, and there is no reference the narration needs to carry."
}
]
}One entry per rail family — mpesa_paybill, and bank_transfer, which covers
PesaLink, RTGS and EFT — and the array can be empty. A partner whose KES
account is not activated yet gets [] rather than a placeholder, because
publishing a reference nothing is watching is money arriving nowhere. The
usdt half of the same response is your USDT deposit address, which tops up
your USDT balance; see
USDT on Tron.
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 KES balance account —
the same one on both rails — so there is nothing per-order to configure.
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.
A deposit is not tied to any order
Every payment on either rail credits your KES balance in general. There is no per-order reference to quote, no rail to declare when you create an order, and nothing here attributes a deposit to one buy order rather than another — because a buy order no longer waits on a payment at all. See Buy USDT with KES: the KES leaves your balance the instant you create the order.
Ask a payer to pay, instead of waiting for them
The rails above are pull-by-instruction: you publish an account and wait. On M-PESA you can also raise the prompt yourself, which turns a message you hope somebody acts on into a notification on their handset.
POST /v1/funding/mpesa-push takes
the number and the amount, and nothing else:
{
"phone": "254700000000",
"amount": "1500"
}{
"pushId": "SBX0EXAMPLE",
"status": "pending"
}What to know before you build on it:
- The destination is not a field. The money lands in your own KES
balance, resolved from your partner record. There is nothing here that can
send money somewhere else, which is why the endpoint needs only
funding:writeand not the ceremonyPOST /v1/transfersneeds. - Nothing has moved when you get the
202. The payer enters their own PIN on their own handset. A prompt they decline or ignore produces no event at all, and there is no route that readspushId— treat it as something to quote to us, not as a resource. - The amount is whole shillings, and capped. M-PESA does not process
decimals on an inbound customer-to-business push, so
"100.50"is refused rather than rounded — as is an amount above the M-PESA per-transaction limit, and a leading zero. All three are a400on the body, refused here rather than forwarded to the bank and refused there. - There is no idempotency key. A repeated call is a second prompt, and if
the payer approves both then both deposits land and each raises its own
credit.received. Guard the retry on your side. - It is rate-limited to 10 a minute, lower than the rest of the API,
because every call rings a real phone. Over the limit is a
429withRetry-After-partner. If you need to collect faster than that, publish the account number instead and let payers push to you. - The two failures mean different things. A
422 MPESA_PUSH_REJECTEDis the bank answering and declining — usually a number that is not M-PESA-registered — so retrying it unchanged will be declined again. A503 MPESA_PUSH_UNAVAILABLEis the bank not answering, which is ambiguous: the prompt may already have gone out. Wait forcredit.receivedbefore retrying that one, or you may raise a second prompt on the same handset. See Errors.
What tells you the money arrived
credit.received fires for every cleared credit on your KES balance,
whatever the rail and whoever sent it — the Pay Bill, a bank transfer, or a
push you raised yourself. It is the signal that tells you a deposit landed the
moment it happened, rather than making you notice a number changed. It is not
the only record, and in rare cases none is sent — when we learn of a credit
through our own reconciliation rather than the bank's notification — so
reconcile against GET /v1/balances periodically as
the backstop.
It is a signed webhook, and it carries a delivery id that is
stable across retries. Its accountNumber names the account that was credited:
match it against the accountNumber on GET /v1/balances. On a Collections
account the same event also covers your USD account, so check accountNumber
(or amount.currency) before you treat a credit as a KES deposit. The KES a
conversion pays you can also arrive as a
credit.received; count that money on conversion.settled, not here — see
Webhooks.
Nothing on this API moves a credit onto an order
There is no attribution step any more. A credit either lands on your balance
or it does not; it is never matched to a buy order, because a buy order is
debited at creation and never waits on one. Do not build a design that expects
order.funds_confirmed for a KES deposit — that event is unrelated to this
rail.
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. credit.received tells you money
arrived; it does not tell you which of your own users sent it.
Read reference off the event: on a bank transfer it is the payer's own
bank reference, on M-PESA it is their receipt code, and it is null if the
rail carried none. Alongside the amount and the time the credit landed, that
is the full set of handles you get — there is no field that names your user,
because we never learn who they are.
If your product asks contributors, customers or members to identify themselves in a payment, design for that set rather than assuming an identifier arrives with the money:
- Reconcile on
reference, amount and time. A payer's M-PESA receipt code or bank reference is the one thing they can quote back to you, so build the matching you need around it rather than a reference we mint — we do not mint one for this rail. - Or raise the payment yourself.
POST /v1/funding/mpesa-pushlets you prompt a specific phone number for a specific amount, so you already know who the payment is for before it lands — the identification problem moves to before the payment rather than after it.
Read next
Guides
Money in, money out, the conversion between them and USD collections: which guide you need, the stages each kind of instruction goes through, and what is true of all of them.
Sell USDT, receive KES
Your USDT is taken from your own balance the instant you create the order. Quote, create, settle — no send step, no hash to attach.