Payouts API
Disburse a merchant's available balance to a mobile-money recipient. Payouts route by currency:
- KES → M-Pesa B2C (
mpesa_b2c) - TZS → Tanzanian mobile money (
mobile_money, via GoDigital — Vodacom, Airtel, Mixx by Yas, Halotel) - UGX → Ugandan mobile money (
mobile_money, via Collecto — MTN, Airtel)
Each currency's payouts must be enabled for your account by an admin before they can be requested.
TZS and UGX payouts carry a flat fee, charged on top of the amount (see Fees):
you're debited amount + fee, and the recipient receives the full amount. KES
(M-Pesa) payouts have no fee today.
The Payout object
{
"id": "d7f3a48c-...",
"amount": 500.00,
"fee": 0.00,
"currency": "KES",
"wallet_id": "wlt_DEFAULT00001",
"channel": "mpesa_b2c",
"status": "succeeded",
"destination_phone": "+254712345678",
"channel_receipt": "PFR9XYZA12",
"callback_url": "https://your-domain.com/callbacks/payout-123",
"created_at": "2026-05-19T11:00:00Z",
"updated_at": "2026-05-19T11:00:18Z"
}| Field | Type | Notes |
|---|---|---|
id | uuid | Payout id |
amount | number | Decimal major units — the amount the recipient receives |
fee | number | Payout fee, decimal major units, charged on top of amount (0 for KES). The merchant is debited amount + fee |
currency | string | ISO-4217. KES → mpesa_b2c; TZS / UGX → mobile_money |
wallet_id | string | Wallet the payout is debited from |
channel | enum | mpesa_b2c (KES) or mobile_money (TZS, UGX) — selected by currency |
status | enum | pending / succeeded / failed |
destination_phone | string | E.164 with + |
channel_receipt | string | Daraja TransactionID — set when succeeded |
failure_reason | string | Set on failed |
callback_url | string | Per-payout callback URL, if one was supplied at creation |
POST /v1/payouts
Request a payout. destination_phone is required — a payout is a disbursement, so every one names the mobile-money number it pays out to.
Request
{
"amount": 500.00,
"currency": "KES",
"destination_phone": "+254712345678",
"remarks": "Daily settlement"
}| Field | Type | Required | Notes |
|---|---|---|---|
amount | number | ✓ | Decimal major units, positive |
destination_phone | string | ✓ | Recipient number — E.164 with a leading +. +254… (KES / M-Pesa), +255… (TZS), +256… (UGX) |
currency | string | ✗ | Defaults to merchant default. Selects the channel: KES→M-Pesa B2C, TZS→Tanzanian mobile money, UGX→Ugandan mobile money |
wallet_id | string | ✗ | Wallet to debit — its wlt_… id or its 8-digit number. Defaults to the default wallet; balance is checked against this wallet |
callback_url | string | ✗ | Absolute https:// URL. When set, this payout's payout.succeeded / payout.failed events are delivered only to this URL (overriding your registered webhook endpoints for this payout), HMAC-signed with your callback signing secret. Omit to fan out to registered endpoints |
remarks | string | ✗ | Sent to Daraja; appears in M-Pesa transaction notes |
Example — Tanzania mobile money (TZS)
channel is chosen by currency — a TZS payout disburses over Tanzanian mobile money via GoDigital, routed to the recipient's network by their number.
curl -X POST https://api.teronapay.com/v1/payouts \
-u "$KEY:$SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: settle-9012" \
-d '{
"amount": 5000.00,
"currency": "TZS",
"destination_phone": "+255712345678"
}'Example — Uganda mobile money (UGX)
A UGX payout disburses over Ugandan mobile money via Collecto. Minimum 5,000 UGX per payout.
curl -X POST https://api.teronapay.com/v1/payouts \
-u "$KEY:$SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: settle-9013" \
-d '{
"amount": 5000,
"currency": "UGX",
"destination_phone": "+256700000000"
}'Idempotency
Send Idempotency-Key: <key> to dedupe retries.
Fees
TZS and UGX payouts are charged a flat fee, charged on top of the payout amount:
| Currency | Flat fee | Minimum payout |
|---|---|---|
| TZS | 1,000 TZS | — |
| UGX | 1,500 UGX | 5,000 UGX |
| KES | none (fee: 0.00) | — |
For example, a 3,000 TZS payout debits 4,000 (amount + fee) and the recipient receives 3,000.
- The
feeis locked onto the payout at creation and returned on thePayoutobject. - The balance check is against
amount + fee— ensure the source wallet holds both. - On success the fee is retained as platform revenue; on a failed payout nothing is charged — the full
amount + feeis released back. - UGX has a 5,000 minimum per payout (a smaller amount is rejected).
Behavior
- We immediately reserve
amount + feefrom the source wallet'smerchant:<id>:wallet:<wallet_id>:payable:<CUR>into its…:payable_pending:<CUR>(the default wallet whenwallet_idis omitted). The reservation must fit the wallet's available balance. - We disburse over the currency's channel — M-Pesa B2C (KES), GoDigital (TZS), or Collecto (UGX) — with our payout id as the correlation reference, and return the payout in status
pending. - The payout is finalized on its real outcome. On
succeeded, the reservedamount + feeclears from…:payable_pending:<CUR>—amountleaves to the recipient andfeeis recognized as platform revenue. Onfailed(or a sync rejection), the fullamount + feereturns tomerchant:<id>:payable:<CUR>— a failed payout costs nothing.
Errors
400 invalid_request— missing/invaliddestination_phone, missing amount, below a currency minimum (e.g. UGX < 5,000), etc422 precondition_failed— available balance <amount + fee502 bad_gateway— the provider (Daraja / GoDigital / Collecto) rejected synchronously (auth, malformed request, etc); payout marked failed inline and the reservation released
GET /v1/payouts
List payouts. Same shape as GET /v1/payments.
Query parameters: status, since, before, limit.
GET /v1/payouts/{id}
Retrieve one payout.
Reconciliation (lost callbacks)
Payouts finalize on their provider outcome. How that outcome is learned depends on the rail:
- KES (M-Pesa B2C) — Daraja has no payout-status query API, so a stuck payout (
pendinglonger thanPAYOUT_SWEEPER_STUCK_AFTER, default 1h) is markedfailedand the reservation released. Trade-off: a payout Daraja secretly succeeded would be reportedfailed; the generous threshold makes this rare. - TZS (GoDigital) / UGX (Collecto) — both expose a transaction-status API, so the payout is polled every ~20s and finalized on its real outcome — typically within a minute of the provider settling it (success releases the disbursement; failure releases the reservation, surfacing the provider's reason). A hard-timeout backstop fails a transaction the provider never terminalizes.
What's NOT supported
- Card payouts. Mobile money only today — M-Pesa B2C (KES), Tanzanian mobile money (TZS), Ugandan mobile money (UGX).
- Bank payouts. Coming.
- Bulk payouts. Issue them one at a time.
- Payouts to a different currency than the source. No FX.
