TeronaPay

Payouts API

The Payout object

{
  "id": "d7f3a48c-...",
  "amount": 500.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
currencystringISO-4217 (KES only for mpesa_b2c)
wallet_idstringWallet the payout is debited from
channelenumToday: mpesa_b2c
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 — B2C is a disbursement, so every payout names the M-Pesa number it pays out to.

Request

{
  "amount": 500.00,
  "currency": "KES",
  "destination_phone": "+254712345678",
  "remarks": "Daily settlement"
}
FieldTypeRequiredNotes
amountnumberDecimal major units, positive
destination_phonestringRecipient M-Pesa number — E.164 with a leading + (e.g. +254712345678)
currencystringDefaults to merchant default
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

Idempotency

Send Idempotency-Key: <key> to dedupe retries.

Behavior

  1. We immediately reserve amount 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).
  2. We call Daraja B2C v3 with a generated OriginatorConversationID. The Daraja ConversationID becomes our channel_request_id.
  3. The response returns the payout in status pending.
  4. Daraja's async callback finalizes the payout. On succeeded, the reservation moves to platform:bank:operating:<CUR>. On failed, it returns to merchant:<id>:payable:<CUR>.

Errors

  • 400 invalid_request — missing/invalid destination_phone, missing amount, etc
  • 422 precondition_failed — balance < amount
  • 502 bad_gateway — Daraja rejected synchronously (auth, malformed request, etc); payout marked failed inline

GET /v1/payouts

List payouts. Same shape as GET /v1/payments.

Query parameters: status, since, before, limit.

GET /v1/payouts/{id}

Retrieve one payout.

Sweeper

A payout sweeper runs every 5 minutes (configurable). If a payout has been pending longer than PAYOUT_SWEEPER_STUCK_AFTER (default 1h), it's marked failed and the reservation released.

Trade-off: a payout that Daraja secretly succeeded will be reported failed to the merchant. The generous threshold makes this rare.

What's NOT supported

  • Card payouts. Today only M-Pesa B2C.
  • 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