Skip to content

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

POST/api/v1/payouts/quote
API keypayouts:read

Price 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.

Body (JSON)
  • amountintegerrequired

    What 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.

  • currencystringrequired

    Always "KES".

    KES
  • source_tokenstringoptional

    Defaults to "USDC".

    USDC
  • destinationobjectrequired

    Where 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.

Request
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"
    }
  }'
Response200 application/json
{
  "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"
  }
}
Response fields
  • ratestring

    KES per USDC.

  • amount_usdcstring

    USDC that converts to the KES amount.

  • fee_usdcstring

    Our fee plus the network fee.

  • total_usdcstring

    What the payout would take from your balance: amount_usdc + fee_usdc.

  • fee_breakdownobject

    service_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 | null

    The account holder's name when the network returns one. Check it against who you meant to pay.

  • expires_atstring

    The price holds until then (about a minute). Creating the payout prices it again; the rate can differ slightly.

  • 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

POST/api/v1/payouts
API keypayouts:write Idempotency-Key required

Send 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.

Body (JSON)
  • amountintegerrequired

    What 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.

  • currencystringrequired

    Always "KES".

    KES
  • source_tokenstringoptional

    Defaults to "USDC".

    USDC
  • destinationobjectrequired

    Where 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.

  • referencestringoptional

    Your 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.

  • descriptionstringoptional

    Up to 140 characters, for your records.

  • metadataobjectoptional

    Up 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.

Request
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"
    }
  }'
Response201 application/json
{
  "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"
  }
}
Response fields
  • idstring

    Unique id, starts with po_. Use it to fetch the payout and to match webhooks.

  • objectstring

    Always "payout".

  • livemodeboolean

    true for live keys (real money), false for test keys.

  • statusstring

    Where 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_review
  • amountinteger

    What the recipient gets, in KES cents. 150000 is KES 1,500.

  • currencystring

    Always "KES" today.

  • source_tokenstring

    The stablecoin the payout is paid from. "USDC".

  • debited_usdcstring

    Total taken from your balance: the amount converted at rate, plus fee_usdc. Decimal string, 6 places.

  • fee_usdcstring

    Our fee plus the network fee, in USDC. Already included in debited_usdc.

  • ratestring

    KES per USDC used for this payout, locked when it was created.

  • destinationobject

    Where 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 | null

    The account holder's name when the network returns one, or the name you sent.

  • referencestring | null

    Your own reference, echoed back. Search by it with GET /api/v1/payouts?reference=…

  • descriptionstring | null

    Your description, echoed back.

  • metadataobject

    Your key-value pairs, echoed back on the payout, its events and webhooks.

  • failure_codestring | null

    Set 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 | null

    A sentence you can show to a person, explaining failure_code.

  • createdstring

    ISO 8601 time the payout was created.

  • updatedstring

    ISO 8601 time of the last status change.

  • 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

GET/api/v1/payouts
API keypayouts:read

Your payouts, newest first, in the key's environment. Filter by status, your reference or a time window.

Query parameters
  • limitintegeroptional

    How many to return, 1 to 100. Default 20.

  • starting_afterstringoptional

    Cursor 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.

  • statusstringoptional

    Only payouts in this status.

    pendingprocessingsucceededfailedreversedin_review
  • referencestringoptional

    Only payouts with this reference.

  • created_gtestringoptional

    Created at or after this ISO 8601 time.

  • created_ltstringoptional

    Created before this ISO 8601 time.

Request
curl "https://api.autopayke.com/api/v1/payouts?limit=20&status=succeeded" \
  -H "Authorization: Bearer $AUTOPAYKE_API_KEY"
Response200 application/json
{
  "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

GET/api/v1/payouts/{id}
API keypayouts:read

One payout by id, with its current status. Use it to recover after a missed webhook or a timeout.

Path parameters
  • idstringrequired

    The payout id (po_…).

Request
curl "https://api.autopayke.com/api/v1/payouts/po_8Fq2mZx7Lk3Rt9Vb" \
  -H "Authorization: Bearer $AUTOPAYKE_API_KEY"
Response200 application/json
{
  "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"
  }
}
Response fields
  • idstring

    Unique id, starts with po_. Use it to fetch the payout and to match webhooks.

  • objectstring

    Always "payout".

  • livemodeboolean

    true for live keys (real money), false for test keys.

  • statusstring

    Where 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_review
  • amountinteger

    What the recipient gets, in KES cents. 150000 is KES 1,500.

  • currencystring

    Always "KES" today.

  • source_tokenstring

    The stablecoin the payout is paid from. "USDC".

  • debited_usdcstring

    Total taken from your balance: the amount converted at rate, plus fee_usdc. Decimal string, 6 places.

  • fee_usdcstring

    Our fee plus the network fee, in USDC. Already included in debited_usdc.

  • ratestring

    KES per USDC used for this payout, locked when it was created.

  • destinationobject

    Where 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 | null

    The account holder's name when the network returns one, or the name you sent.

  • referencestring | null

    Your own reference, echoed back. Search by it with GET /api/v1/payouts?reference=…

  • descriptionstring | null

    Your description, echoed back.

  • metadataobject

    Your key-value pairs, echoed back on the payout, its events and webhooks.

  • failure_codestring | null

    Set 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 | null

    A sentence you can show to a person, explaining failure_code.

  • createdstring

    ISO 8601 time the payout was created.

  • updatedstring

    ISO 8601 time of the last status change.

Errors

Plus the key and rate errors any keyed call can return.

  • A test key can't see live payouts and the other way round: you'll get NOT_FOUND.