OnLink
Guides

Collect in the US, pay out in Kenya

End to end: take USD from US contributors by ACH, FedNow or wire, then convert and pay out KES to M-PESA, a bank account or PesaLink.

Your contributors are in the US. Your recipients are in Kenya. This guide joins the two halves of that into one sequence, and points at the reference pages for each call.

Collections and the USDT rails are separate products with separate scopes — see Products and scopes for which credential you need. An account doing the full journey below holds both.

The sequence

  1. Open a collection — you get a reference and a set of US deposit details.
  2. Your contributor sends USD by ACH, FedNow or wire.
  3. We attribute the deposit, take the disclosed fee and credit you the net. You learn who paid.
  4. You convert USD to KES against a quoted rate.
  5. You pay out to M-PESA, a bank account or PesaLink.

Every step also emits a signed webhook, so nothing here needs polling.

Part 1 — collecting in the US

Open one collection per payer you want to tell apart

POST /v1/collections
Content-Type: application/json

{
  "currency": "USD",
  "expectedMinor": 50000
}

expectedMinor is advisory. A deposit that does not match it is still attributed and still credited — we report the difference rather than refuse the money.

The response carries a reference that is yours to quote to your own customer, and the deposit details to hand your contributor:

{
  "reference": "ACME-7K3Q2M",
  "status": "open",
  "currency": "USD",
  "depositInstructions": {
    "beneficiaryName": "<beneficiary name>",
    "bankName": "<bank name>",
    "routingNumber": "000000000",
    "accountNumber": "00000000123456",
    "rails": ["ach_push", "fednow", "wire"],
    "depositMessage": "BRG7TYAG73RW43DYGDU77"
  }
}

Return the deposit details verbatim. Do not reformat the account number, and do not substitute your own name for beneficiaryName — these are the values the contributor's bank matches on.

The depositMessage is how the money finds you

Most accounts share one account number across many references, and depositMessage is what separates them. Your contributor has to include it in the transfer — the wire memo, or the ACH description.

A deposit that arrives without it matches no collection. It is not credited, and it does not appear on the collection at any status; it has to be reconciled by hand or returned. Put the code in front of your payer at the moment they set the transfer up, not in a follow-up email.

Some accounts are configured with a dedicated account number per collection instead, and then no code is needed — depositMessage is simply absent. Branch on whether the field is present rather than assuming one shape.

Read the deposit back

When the money lands, the collection carries the deposit. Read it back, or wait for the collection webhook — Webhooks lists the events your account can receive:

FieldWhat it is
referencethe one you handed out
grossMinorwhat the contributor sent
feeMinorthe disclosed fee
netMinorwhat you keep
senderNamewho actually paid
senderBankName, senderBankRoutingNumbertheir bank

The payer fields are the ones worth building against. When the name on a transfer is not the name on your customer's account, they are how you reconcile the two.

You can also read a collection at any time with GET /v1/collections/{id}, or list them filtered by reference, status or date.

Part 2 — paying out in Kenya

The net sits in your USD balance. Three calls land KES:

  1. POST /v1/quotes — lock a rate. The response carries its own TTL.
  2. POST /v1/orders — sell against that quote and receive KES.
  3. POST /v1/transfers — pay out.

Payout rails: M-PESA, bank account, PesaLink, RTGS and EFT. See Send money from your KES account for the per-rail fields, and Send a batch of transfers when you are paying many recipients at once.

A quote expires. Read expiresAt and re-quote rather than retrying an order against a stale one.

Webhooks

The payout half emits, in order, order.funds_confirmed, order.settled and then transfer.settled. Failures arrive as order.rejected, order.expired or transfer.failed. The collection half emits its own event when a deposit is credited; Webhooks is the catalogue of what your account can receive, and it is the page to build your handler against.

Two properties to build against:

  • Delivery is at-least-once. Every delivery carries a stable id that does not change across retries. Dedupe on it, and a retry reads as the same event rather than a new one.
  • Every delivery is signed. Verify X-OnLink-Signature before acting on a body. Webhooks has the string-to-sign and a worked verifier.

Fees

The collections fee is set on your account and disclosed on every collection:

fee = min(cap, ceil(gross × percent_bps / 10000) + flat)
net = gross − fee

It rounds up, and it comes off the contributor's side — netMinor is what reaches you. The cap is optional. The conversion leg is priced separately and quoted before you commit to it.

Availability

Collections is in limited release and is enabled per account. The USDT rails and KES payouts in Part 2 are generally available.

Talk to us about turning collections on, and tell us your expected volume and typical contribution size — that decides whether a shared account with a depositMessage or a dedicated account number per collection is the better fit for you, and it sets your fee terms.

On this page