Payouts
Send KES to M-Pesa, Airtel Money, Till (Buy Goods), PayBill and Kenyan bank accounts, paid from your USDC balance. Quote first, then create, then follow the status.
A payout moves through pending → processing → succeeded. It can end failed (nothing was paid; any debit is returned) or, after succeeding, reversed (the money came back). Some payouts pause in in_review for a manual check.
Every create needs an Idempotency-Key header, so retrying after a timeout can never pay twice. Get the final status from webhooks (payout.succeeded, payout.failed) rather than polling.
Send your API key as Authorization: Bearer apk_test_… (sandbox) or apk_live_… (live). Each endpoint lists the scope the key needs. See API keys.
Quote a payout
/api/v1/payouts/quotepayouts:readPrice a payout without sending it: the rate, our fee and the total USDC it will take from your balance. It also checks the destination and your limits, so a quote that succeeds means the payout would be accepted now.
amountintegerrequiredWhat the recipient receives, in KES cents, whole shillings only (a multiple of 100). 150000 sends KES 1,500. Fees are added on top and taken from your balance.
currencystringrequiredAlways "KES".
KESsource_tokenstringoptionalDefaults to "USDC".
USDCdestinationobjectrequiredWhere to send the money. Pick one shape by "type". mobile_money: { type, phone, network?, name? } where phone is a Kenyan mobile number in any common format (+254712345678, 0712345678), network is "safaricom" (default) or "airtel", and name is the name you expect on the account (optional, used for checks). till: { type, till_number } with a 5 to 8 digit Buy Goods number. paybill: { type, paybill_number, account_number } with a 5 to 7 digit PayBill and the account number the business gave (1 to 20 characters). bank: { type, bank_code, account_number, account_name } where bank_code comes from GET /api/v1/banks, account_number is 6 to 34 characters and account_name is the holder's name.
curl -X POST "https://api.autopayke.com/api/v1/payouts/quote" \
-H "Authorization: Bearer $AUTOPAYKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 150000,
"currency": "KES",
"destination": {
"type": "mobile_money",
"phone": "+254700000001"
}
}'{
"ok": true,
"data": {
"object": "payout_quote",
"livemode": false,
"amount": 150000,
"currency": "KES",
"source_token": "USDC",
"rate": "128.80",
"amount_usdc": "11.646000",
"fee_usdc": "0.302310",
"total_usdc": "11.948310",
"fee_breakdown": {
"service_fee_usdc": "0.291150",
"network_fee_usdc": "0.011160",
"payout_cost_usdc": "0.000000"
},
"recipient_name": null,
"expires_at": "2026-11-03T09:11:00.000Z"
}
}ratestringKES per USDC.
amount_usdcstringUSDC that converts to the KES amount.
fee_usdcstringOur fee plus the network fee.
total_usdcstringWhat the payout would take from your balance: amount_usdc + fee_usdc.
fee_breakdownobjectservice_fee_usdc (our fee, already 0.5 points below the app price for API partners), network_fee_usdc (Avalanche gas), payout_cost_usdc (the payout network's charge when it's passed through; 0 when it's inside the service fee).
recipient_namestring | nullThe account holder's name when the network returns one. Check it against who you meant to pay.
expires_atstringThe price holds until then (about a minute). Creating the payout prices it again; the rate can differ slightly.
VALIDATION_ERRORRECIPIENT_INVALIDMETHOD_UNAVAILABLELIMIT_PER_TX_EXCEEDEDLIMIT_DAILY_EXCEEDEDLIMIT_MONTHLY_EXCEEDEDCAPABILITY_PAUSEDPlus the key and rate errors any keyed call can return.
- A quote isn't a reservation: it doesn't hold money or a rate. It's free and doesn't count against limits.
Create a payout
/api/v1/payoutspayouts:write Idempotency-Key requiredSend money. We check the destination, your limits and your balance, take the total from your balance and start the payout. It returns at once with status pending; the result arrives by webhook, usually within a minute for mobile money.
amountintegerrequiredWhat the recipient receives, in KES cents, whole shillings only (a multiple of 100). 150000 sends KES 1,500. Fees are added on top and taken from your balance.
currencystringrequiredAlways "KES".
KESsource_tokenstringoptionalDefaults to "USDC".
USDCdestinationobjectrequiredWhere to send the money. Pick one shape by "type". mobile_money: { type, phone, network?, name? } where phone is a Kenyan mobile number in any common format (+254712345678, 0712345678), network is "safaricom" (default) or "airtel", and name is the name you expect on the account (optional, used for checks). till: { type, till_number } with a 5 to 8 digit Buy Goods number. paybill: { type, paybill_number, account_number } with a 5 to 7 digit PayBill and the account number the business gave (1 to 20 characters). bank: { type, bank_code, account_number, account_name } where bank_code comes from GET /api/v1/banks, account_number is 6 to 34 characters and account_name is the holder's name.
referencestringoptionalYour own id for this payout, up to 64 characters (an invoice, order or payroll line). Shown on the payout and searchable. Not unique: use Idempotency-Key for that.
descriptionstringoptionalUp to 140 characters, for your records.
metadataobjectoptionalUp to 20 string key-value pairs (keys up to 40 characters, values up to 500). Returned on the payout, its events and webhooks. Don't put secrets or card numbers here.
curl -X POST "https://api.autopayke.com/api/v1/payouts" \
-H "Authorization: Bearer $AUTOPAYKE_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 150000,
"currency": "KES",
"destination": {
"type": "mobile_money",
"phone": "+254700000001",
"network": "safaricom",
"name": "Jane Wanjiku"
},
"reference": "order_1042",
"description": "Rider payout, week 44",
"metadata": {
"rider_id": "r_5521"
}
}'{
"ok": true,
"data": {
"id": "po_8Fq2mZx7Lk3Rt9Vb",
"object": "payout",
"livemode": false,
"status": "pending",
"amount": 150000,
"currency": "KES",
"source_token": "USDC",
"debited_usdc": "11.948310",
"fee_usdc": "0.302310",
"rate": "128.80",
"destination": {
"type": "mobile_money",
"network": "safaricom",
"phone": "+254 700 ••• 001"
},
"recipient_name": "Jane Wanjiku",
"reference": "order_1042",
"description": "Rider payout, week 44",
"metadata": {
"rider_id": "r_5521"
},
"failure_code": null,
"failure_message": null,
"created": "2026-11-03T09:10:00.000Z",
"updated": "2026-11-03T09:10:00.000Z"
}
}idstringUnique id, starts with po_. Use it to fetch the payout and to match webhooks.
objectstringAlways "payout".
livemodebooleantrue for live keys (real money), false for test keys.
statusstringWhere the payout is. pending: accepted, not yet sent. processing: on its way. succeeded: the recipient has the money. failed: it didn't go through and nothing was paid (any debit is returned). reversed: it succeeded, then the money came back. in_review: held for a manual check before it's paid.
pendingprocessingsucceededfailedreversedin_reviewamountintegerWhat the recipient gets, in KES cents. 150000 is KES 1,500.
currencystringAlways "KES" today.
source_tokenstringThe stablecoin the payout is paid from. "USDC".
debited_usdcstringTotal taken from your balance: the amount converted at rate, plus fee_usdc. Decimal string, 6 places.
fee_usdcstringOur fee plus the network fee, in USDC. Already included in debited_usdc.
ratestringKES per USDC used for this payout, locked when it was created.
destinationobjectWhere the money went, masked: phone numbers and account numbers show only their last digits. Same type as you sent (mobile_money, till, paybill, bank); bank destinations add bank_name.
recipient_namestring | nullThe account holder's name when the network returns one, or the name you sent.
referencestring | nullYour own reference, echoed back. Search by it with GET /api/v1/payouts?reference=…
descriptionstring | nullYour description, echoed back.
metadataobjectYour key-value pairs, echoed back on the payout, its events and webhooks.
failure_codestring | nullSet when status is failed: RECIPIENT_INVALID (number or account can't receive), METHOD_UNAVAILABLE (network didn't answer; nothing was paid), FUNDING_FAILED (couldn't be funded from your wallet; nothing left it), PAYOUT_FAILED (couldn't be completed; any debit is returned), PAYOUT_RETURNED (sent back to your wallet).
failure_messagestring | nullA sentence you can show to a person, explaining failure_code.
createdstringISO 8601 time the payout was created.
updatedstringISO 8601 time of the last status change.
IDEMPOTENCY_KEY_REQUIREDIDEMPOTENCY_KEY_REUSEDIDEMPOTENCY_IN_PROGRESSVALIDATION_ERRORRECIPIENT_INVALIDMETHOD_UNAVAILABLELIMIT_PER_TX_EXCEEDEDLIMIT_DAILY_EXCEEDEDLIMIT_MONTHLY_EXCEEDEDINSUFFICIENT_BALANCECAPABILITY_PAUSEDPlus the key and rate errors any keyed call can return.
- A retry with the same Idempotency-Key and body returns the first response, with the header Idempotent-Replayed: true. The same key with a different body is refused with IDEMPOTENCY_KEY_REUSED.
- If the request fails validation or a limit, nothing is charged and the Idempotency-Key is released, so you can fix the request and retry with the same key.
- Live keys that can create payouts must have an IP allowlist; calls from other addresses get IP_NOT_ALLOWED.
- Sandbox: +254700000001 succeeds, +254700000002 fails, +254700000003 goes to in_review, +254700000004 succeeds then reverses, an amount of 13300 (KES 133) times out after a minute, and account number 0000000000 fails. Anything else succeeds about 5 seconds later.
List payouts
/api/v1/payoutspayouts:readYour payouts, newest first, in the key's environment. Filter by status, your reference or a time window.
limitintegeroptionalHow many to return, 1 to 100. Default 20.
starting_afterstringoptionalCursor for the next page: the id of the last item you got. Lists are newest first; when has_more is true, pass the last id here.
statusstringoptionalOnly payouts in this status.
pendingprocessingsucceededfailedreversedin_reviewreferencestringoptionalOnly payouts with this reference.
created_gtestringoptionalCreated at or after this ISO 8601 time.
created_ltstringoptionalCreated before this ISO 8601 time.
curl "https://api.autopayke.com/api/v1/payouts?limit=20&status=succeeded" \
-H "Authorization: Bearer $AUTOPAYKE_API_KEY"{
"ok": true,
"data": {
"object": "list",
"data": [
{
"id": "po_8Fq2mZx7Lk3Rt9Vb",
"object": "payout",
"livemode": false,
"status": "succeeded",
"amount": 150000,
"currency": "KES",
"source_token": "USDC",
"debited_usdc": "11.948310",
"fee_usdc": "0.302310",
"rate": "128.80",
"destination": {
"type": "mobile_money",
"network": "safaricom",
"phone": "+254 700 ••• 001"
},
"recipient_name": "Jane Wanjiku",
"reference": "order_1042",
"description": "Rider payout, week 44",
"metadata": {
"rider_id": "r_5521"
},
"failure_code": null,
"failure_message": null,
"created": "2026-11-03T09:10:00.000Z",
"updated": "2026-11-03T09:10:00.000Z"
}
],
"has_more": false
}
}- To walk every page: call with limit=100, then repeat with starting_after set to the last id while has_more is true.
Get a payout
/api/v1/payouts/{id}payouts:readOne payout by id, with its current status. Use it to recover after a missed webhook or a timeout.
idstringrequiredThe payout id (po_…).
curl "https://api.autopayke.com/api/v1/payouts/po_8Fq2mZx7Lk3Rt9Vb" \
-H "Authorization: Bearer $AUTOPAYKE_API_KEY"{
"ok": true,
"data": {
"id": "po_8Fq2mZx7Lk3Rt9Vb",
"object": "payout",
"livemode": false,
"status": "succeeded",
"amount": 150000,
"currency": "KES",
"source_token": "USDC",
"debited_usdc": "11.948310",
"fee_usdc": "0.302310",
"rate": "128.80",
"destination": {
"type": "mobile_money",
"network": "safaricom",
"phone": "+254 700 ••• 001"
},
"recipient_name": "Jane Wanjiku",
"reference": "order_1042",
"description": "Rider payout, week 44",
"metadata": {
"rider_id": "r_5521"
},
"failure_code": null,
"failure_message": null,
"created": "2026-11-03T09:10:00.000Z",
"updated": "2026-11-03T09:10:00.000Z"
}
}idstringUnique id, starts with po_. Use it to fetch the payout and to match webhooks.
objectstringAlways "payout".
livemodebooleantrue for live keys (real money), false for test keys.
statusstringWhere the payout is. pending: accepted, not yet sent. processing: on its way. succeeded: the recipient has the money. failed: it didn't go through and nothing was paid (any debit is returned). reversed: it succeeded, then the money came back. in_review: held for a manual check before it's paid.
pendingprocessingsucceededfailedreversedin_reviewamountintegerWhat the recipient gets, in KES cents. 150000 is KES 1,500.
currencystringAlways "KES" today.
source_tokenstringThe stablecoin the payout is paid from. "USDC".
debited_usdcstringTotal taken from your balance: the amount converted at rate, plus fee_usdc. Decimal string, 6 places.
fee_usdcstringOur fee plus the network fee, in USDC. Already included in debited_usdc.
ratestringKES per USDC used for this payout, locked when it was created.
destinationobjectWhere the money went, masked: phone numbers and account numbers show only their last digits. Same type as you sent (mobile_money, till, paybill, bank); bank destinations add bank_name.
recipient_namestring | nullThe account holder's name when the network returns one, or the name you sent.
referencestring | nullYour own reference, echoed back. Search by it with GET /api/v1/payouts?reference=…
descriptionstring | nullYour description, echoed back.
metadataobjectYour key-value pairs, echoed back on the payout, its events and webhooks.
failure_codestring | nullSet when status is failed: RECIPIENT_INVALID (number or account can't receive), METHOD_UNAVAILABLE (network didn't answer; nothing was paid), FUNDING_FAILED (couldn't be funded from your wallet; nothing left it), PAYOUT_FAILED (couldn't be completed; any debit is returned), PAYOUT_RETURNED (sent back to your wallet).
failure_messagestring | nullA sentence you can show to a person, explaining failure_code.
createdstringISO 8601 time the payout was created.
updatedstringISO 8601 time of the last status change.
- A test key can't see live payouts and the other way round: you'll get NOT_FOUND.