Skip to main content
A collection is the money-in entry point: what your app calls when a user taps “Pay”. You hand over an amount, a country, a method and the payer’s identifier, and the gateway asks the routed provider to debit them. Typical uses are a checkout, a wallet top-up or an entry fee. The customer-facing prompt — the mobile money PIN, the redirect page, the SMS code — belongs to the provider. You get back a canonical status to branch on instead of a provider-shaped payload.
A charge does not settle synchronously. POST /v1/collections returns 201 with status: "pending", which means the request reached the provider, not that the customer has paid. Treat the webhook, or a poll of GET /v1/collections/{reference}, as the real answer. Never treat the 201 as settlement.

Before you start

  • Amounts are integer minor units. Every amount field ends in _minor. UGX, RWF, XAF and XOF are zero-decimal, so 2,000 UGX is 2000, not 200000. Getting this wrong is a 100x error. See Money and minor units.
  • Check the corridor. Capability is per (country, currency, direction, method). Call GET /v1/reference/corridors and render your UI against it.
  • Idempotency-Key is required. This call moves money. See Idempotency.

The flow

1

Quote the charge (optional)

POST /v1/collections/quote returns the fee and the total the customer must pay, so a confirm screen can show real numbers before anyone commits.This is a standalone call that holds no state. It moves no money, needs no Idempotency-Key, and returns no token to redeem — quote as often as you like, or skip it entirely. A quote is never “used up” and can never expire out from under a customer.Show total_to_pay_minor rather than adding amount_minor and fee_minor yourself. That figure comes from the provider and is what the customer is actually billed.
Quote a collection
momo and bank can be quoted. card cannot, and answers 501.
2

Charge the customer

One call. Send the amount, the corridor and the payer’s number.
Charge a Uganda mobile money wallet
The response carries a gateway reference. Persist it: it is your handle for polling, for the webhook you will receive, and for a later refund.
Response
The customer now sees a prompt on their handset and approves it there.
3

Branch on next_action

Most charges go straight to the provider and come back pending. Some corridors need a second step from the customer first, and those come back status: "requires_action" with a next_action object.Always branch on the presence of next_action rather than assuming it is absent. That way your integration keeps working if a corridor changes, or if you deploy against a provider that does require a step.Ghana (GHS) requires redirect_url on the request and answers requires_action with a next_action.redirect_url. Send the user there to finish.
4

Wait for the real answer

Your webhook handler is the primary signal and the source of truth for settlement. See Webhooks.As a fallback, poll GET /v1/collections/{reference}. By default that reads the gateway’s own ledger — a database read, not a provider call — so polling is cheap and safe.Pass live=true when a customer is watching a payment screen. The gateway then asks the provider what it currently holds and applies the answer before responding, so a charge the customer just approved is reported as successful on that very poll rather than a minute later. Upstream calls are collapsed to at most one per reference every few seconds however many callers ask, so polling on a timer and from several tabs at once is safe.
Poll with a live upstream check
A live check never changes the response shape and never fails the read. An unreachable provider, an unlookupable transaction, an already-terminal record or a caller inside the cooldown all return the stored record with a 200. The only difference is that the status may be fresher.

A complete Node example

collect.js

Correlate on metadata, not on the reference

Put your own order id in metadata at charge time. It is stored on the transaction and echoed back on every webhook delivery as data.metadata, which is how you map a gateway reference to one of your own. Our reference is opaque. It looks like APT-LCS-oRUZWq8fyn9DttNb3SN3B, and while that shape is APT-{prefix}-{nanoid} today, it carries no field you are meant to parse.
Never parse the reference to recover your own identifiers. Match on data.metadata instead. metadata must serialize to at most 4096 bytes, because it is re-sent on every delivery attempt.

OTP is not required

This account is whitelisted for phone verification, so no corridor currently requires an OTP. requires_otp is false for every corridor. A mobile money charge goes to the provider on the single POST /v1/collections call and the customer approves it on their handset prompt alone — Kenya, Rwanda, Uganda and Ghana alike. There is nothing to collect and nothing to post back. The ladder behind it is retained, and can be re-enabled per corridor through configuration. If a corridor is configured to demand a code, a charge sent without an otp texts the customer one, records the transaction, and answers 201 with status: "requires_action" and a next_action of type otp carrying a pin_id.
Collect the code in your UI and submit it against the transaction’s reference. pin_id comes from the next_action on the charge; pin is what the customer typed.
Authorize with the customer's code
This call validates the code and charges. There is no separate authorize step upstream: authorizing re-sends the charge with the code attached, so a correct code debits the customer immediately. Expect pending here too, and treat the webhook as the answer. A wrong code fails this call without failing the transaction — prompt again and retry.You must resupply source. Re-sending the charge needs the payer’s full number, and the gateway never stores one — only its last four digits, so a leaked database cannot become a list of customer phone numbers. The last4 of what you send is checked against the transaction, so a reference cannot be redirected to a different payer. Amount, currency and country come from the stored transaction and cannot be changed here.
You almost certainly do not need this. POST /v1/collections/otp triggers an SMS one-time PIN for a mobile money collection and is kept for corridors configured to demand a code, and for deployments on an un-whitelisted account.When you do need it: call it first, show the user an “enter the code we texted you” field, then pass what they type back on the charge. It returns a pin_id; send it with the code as otp: {pin_id, pin} — on POST /v1/collections for a fresh charge, or on the authorize endpoint for one already sitting at requires_action.No money moves, so no Idempotency-Key. Calling it again simply sends another code.

The double-charge guard

Before charging, the gateway checks whether a mobile money charge to this same number is already live for your tenant within the last 15 minutes. If one is, it asks the provider what actually happened. This is separate from Idempotency-Key. That protects a retried API call; this protects a retried customer action, which arrives with a brand-new key.

Phone numbers

Send the number in whatever form the customer typed. All of 0772123456, 256772123456, +256772123456 and the bare national 772123456 are accepted, with spaces, dashes and parentheses ignored. The gateway normalises to E.164 with a leading + using the country code of the resolved corridor, so a missing country code is supplied rather than guessed from the digits. On a collection, source.country is optional and defaults to the request’s top-level country.

Errors worth handling

Full list and response shape: Errors.

Next steps

Send a payout

The money-out side, and how float and fees work.

Test your integration

Test keys, the amount ceiling, and the Postman collection.