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"
}| Field | Type | Notes |
|---|---|---|
id | uuid | Payout id |
amount | number | Decimal major units |
currency | string | ISO-4217 (KES only for mpesa_b2c) |
wallet_id | string | Wallet the payout is debited from |
channel | enum | Today: mpesa_b2c |
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 — 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"
}| Field | Type | Required | Notes |
|---|---|---|---|
amount | number | ✓ | Decimal major units, positive |
destination_phone | string | ✓ | Recipient M-Pesa number — E.164 with a leading + (e.g. +254712345678) |
currency | string | ✗ | Defaults to merchant default |
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 |
Idempotency
Send Idempotency-Key: <key> to dedupe retries.
Behavior
- We immediately reserve
amountfrom the source wallet'smerchant:<id>:wallet:<wallet_id>:payable:<CUR>into its…:payable_pending:<CUR>(the default wallet whenwallet_idis omitted). - We call Daraja B2C v3 with a generated
OriginatorConversationID. The DarajaConversationIDbecomes ourchannel_request_id. - The response returns the payout in status
pending. - Daraja's async callback finalizes the payout. On
succeeded, the reservation moves toplatform:bank:operating:<CUR>. Onfailed, it returns tomerchant:<id>:payable:<CUR>.
Errors
400 invalid_request— missing/invaliddestination_phone, missing amount, etc422 precondition_failed— balance < amount502 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.
