OnLink
Guides

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

RailWhat the payer doesWhat attributes the payment
M-PESA Pay BillPays the Pay Bill, quoting your account numberThe account number, plus the exact amount inside the window
Bank transfer (PesaLink, RTGS, EFT)Transfers to the account number we publishThe 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_confirmed

In order:

  1. You create the buy order, declaring paymentRail, and read paymentInstructions for that rail off the response.
  2. You show your payer the account number — and, on a bank transfer, the reference we issued.
  3. Your payer pays, on the rail you declared.
  4. The rail credits the account we issued you.
  5. We attribute the credit to your order, by the reference on a bank transfer or by the amount and the window on M-PESA.
  6. 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. paymentRail is fixed at creation, and the instructions you get back are for that rail alone.
  • Pay the exact amount, before the deadline. The deadline is expiresAt on 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.

On this page