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
- Open a collection — you get a reference and a set of US deposit details.
- Your contributor sends USD by ACH, FedNow or wire.
- We attribute the deposit, take the disclosed fee and credit you the net. You learn who paid.
- You convert USD to KES against a quoted rate.
- 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:
| Field | What it is |
|---|---|
reference | the one you handed out |
grossMinor | what the contributor sent |
feeMinor | the disclosed fee |
netMinor | what you keep |
senderName | who actually paid |
senderBankName, senderBankRoutingNumber | their 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:
POST /v1/quotes— lock a rate. The response carries its own TTL.POST /v1/orders— sell against that quote and receive KES.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-Signaturebefore 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 − feeIt 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.