How Deposits Work
Five steps, whichever country you're accepting payments from. Full field-by-field reference is in Deposits — this page is the shape of the flow.
Don't want to build the form + webhook handling yourself? Hosted Checkout gives you a single iframe that does all five steps below for you, including automatic voucher issuance. Keep reading if you're integrating directly against the API.
| Customer's currency | Endpoint |
|---|---|
| KES (Kenya) | POST /api/v1/stk_push |
| UGX (Uganda) or TZS (Tanzania) | POST /api/v1/xb/stk_push |
They're not interchangeable — sending KES to the XB endpoint (or vice versa) is rejected. Everything below applies to both; where the field names or values differ, it's called out.
Generate an internal reference in your system before calling us — an order ID, invoice number, whatever your system already uses.
your internal order_idyour user reference
You do send this to PesaVoucher (see Step 2) — we echo it back in both the initiation response and the final webhook, so there's nothing to manually reconcile on your end.
KES:
{"phone_number": "254712345678","amount": 1000.00,"account_reference": "ORD-2026-001","order_id": "ORD-2026-001","transaction_desc": "Deposit"}
UGX / TZS:
{"currency": "UGX","phone_number": "256700123456","amount": 25000,"api_ref": "ORD-2026-001"}
There's no per-request
callback_urlfield — your webhook destination is set up on your merchant account, not sent with every call. If yours isn't configured yet, check with support before going live.
KES returns a payment_id (our reference) plus checkout_request_id, with your order_id echoed back:
{"data": {"payment_id": "550e8400-e29b-41d4-a716-446655440000","checkout_request_id": "ws_CO_20112025143022123","order_id": "ORD-2026-001"}}
UGX/TZS returns an invoice_id, with your api_ref echoed back:
{"data": {"invoice_id": "9f2c1e6a-...","api_ref": "ORD-2026-001"}}
Nothing further to store beyond what you already had in Step 1 — the reference you sent is what comes back in the webhook too.
Your order_id / api_ref from Step 2 comes back in the payload, so you can match it straight to your own record without a lookup table.
KES — status field:
{"payment_id": "550e8400-e29b-41d4-a716-446655440000","order_id": "ORD-2026-001","status": "Success","actual_amount": 1000.00}
UGX/TZS — nested invoice.state:
{"invoice": {"invoice_id": "9f2c1e6a-...","api_ref": "ORD-2026-001","state": "COMPLETE","value": "25000"}}
Only credit on a genuine success state — and note the two rails use different words for it:
| Rail | Success value | Don't credit on |
|---|---|---|
| KES | Success | Failed, Cancelled, Timeout |
| UGX/TZS | COMPLETE | FAILED, CANCELLED, FAILED_VALIDATE |
Always rely on the webhook to confirm final status — never credit based on the initiation response in Step 3, which only confirms the request was accepted, not that the customer actually paid.
- Your own reference (
order_id/api_ref) is sent to us and comes back in both the response and the webhook — no manual reconciliation needed - KES and UGX/TZS are separate endpoints with separate webhook field names — check which one you're handling before reading
status - Credit the user only on the webhook's success state, never on the initiation response