Skip to Content
Pesa VoucherDeveloper Documentation

How Deposits Work

Step-by-step guide on handling PesaVoucher deposits and crediting users safely, for KES (M-Pesa) and cross-border (Uganda/Tanzania) payments.


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.

Step 0 – Pick the Right Endpoint

Customer's currencyEndpoint
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.

Step 1 – Create Your Own Order Reference

Generate an internal reference in your system before calling us — an order ID, invoice number, whatever your system already uses.

your internal order_id
your 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.

Step 2 – Send the Deposit Request

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_url field — 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.

Step 3 – We Return a Reference

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.

Step 4 – Wait for the Webhook

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.

KESstatus 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"
}
}

Step 5 – Credit the User

Only credit on a genuine success state — and note the two rails use different words for it:

RailSuccess valueDon't credit on
KESSuccessFailed, Cancelled, Timeout
UGX/TZSCOMPLETEFAILED, 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.

✅ Summary

  • 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