OnLink
Guides

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:write and not the ceremony POST /v1/transfers needs.
  • 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 reads pushId — 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 a 400 on 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 429 with Retry-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_REJECTED is the bank answering and declining — usually a number that is not M-PESA-registered — so retrying it unchanged will be declined again. A 503 MPESA_PUSH_UNAVAILABLE is the bank not answering, which is ambiguous: the prompt may already have gone out. Wait for credit.received before 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-push lets 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.

On this page