TeronaPay

Payouts API

Disburse a merchant's available balance to a mobile-money recipient. Payouts route by currency:

  • KESM-Pesa B2C (mpesa_b2c)
  • TZSTanzanian mobile money (mobile_money, via GoDigital — Vodacom, Airtel, Mixx by Yas, Halotel)
  • UGXUgandan 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"
}
FieldTypeNotes
iduuidPayout id
amountnumberDecimal major units — the amount the recipient receives
feenumberPayout fee, decimal major units, charged on top of amount (0 for KES). The merchant is debited amount + fee
currencystringISO-4217. KESmpesa_b2c; TZS / UGXmobile_money
wallet_idstringWallet the payout is debited from
channelenummpesa_b2c (KES) or mobile_money (TZS, UGX) — selected by currency
statusenumpending / succeeded / failed
destination_phonestringE.164 with +
channel_receiptstringDaraja TransactionID — set when succeeded
failure_reasonstringSet on failed
callback_urlstringPer-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"
}
FieldTypeRequiredNotes
amountnumberDecimal major units, positive
destination_phonestringRecipient number — E.164 with a leading +. +254… (KES / M-Pesa), +255… (TZS), +256… (UGX)
currencystringDefaults to merchant default. Selects the channel: KES→M-Pesa B2C, TZS→Tanzanian mobile money, UGX→Ugandan mobile money
wallet_idstringWallet to debit — its wlt_… id or its 8-digit number. Defaults to the default wallet; balance is checked against this wallet
callback_urlstringAbsolute 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
remarksstringSent 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:

CurrencyFlat feeMinimum payout
TZS1,000 TZS
UGX1,500 UGX5,000 UGX
KESnone (fee: 0.00)

For example, a 3,000 TZS payout debits 4,000 (amount + fee) and the recipient receives 3,000.

  • The fee is locked onto the payout at creation and returned on the Payout object.
  • 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 + fee is released back.
  • UGX has a 5,000 minimum per payout (a smaller amount is rejected).

Behavior

  1. We immediately reserve amount + fee from the source wallet's merchant:<id>:wallet:<wallet_id>:payable:<CUR> into its …:payable_pending:<CUR> (the default wallet when wallet_id is omitted). The reservation must fit the wallet's available balance.
  2. 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.
  3. The payout is finalized on its real outcome. On succeeded, the reserved amount + fee clears from …:payable_pending:<CUR>amount leaves to the recipient and fee is recognized as platform revenue. On failed (or a sync rejection), the full amount + fee returns to merchant:<id>:payable:<CUR> — a failed payout costs nothing.

Errors

  • 400 invalid_request — missing/invalid destination_phone, missing amount, below a currency minimum (e.g. UGX < 5,000), etc
  • 422 precondition_failed — available balance < amount + fee
  • 502 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 (pending longer than PAYOUT_SWEEPER_STUCK_AFTER, default 1h) is marked failed and the reservation released. Trade-off: a payout Daraja secretly succeeded would be reported failed; 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.
API: Payouts · TeronaPay docs