PesaVoucher Paybill Integration Guide
This document explains what a merchant should expect when a customer pays via the PesaVoucher paybill toolkit, and what payload will be sent to the merchant's callback URL once a transaction resolves.
STK Push (the "prompt on your phone" flow) depends on Safaricom's push gateway, which can occasionally be slow or unavailable. The paybill toolkit itself is a separate, always-on channel — it does not depend on the STK push gateway being healthy. Because of this, paybill should be treated as the reliable fallback deposit method: even during periods where STK Push initiation is failing or degraded, customers can still pay directly via paybill and the transaction will be picked up normally.
This also means merchants should surface the paybill number and account number to customers as a standing deposit option, not just as a backup shown only during outages.
Customers are not limited to M-Pesa. Any banking app or platform that supports paying a Safaricom paybill (bank apps, other mobile money apps, etc.) can be used to deposit, by entering:
- Paybill/Business number: PesaVoucher's paybill number
- Account number: the customer's player ID
The account number field is how the deposit gets matched to the correct player — the customer must enter their player ID as the account number for the deposit to be attributed correctly. This is true regardless of which app or bank they're paying from.
- A customer initiates a payment either via STK Push (prompt on their phone) or by paying directly through the paybill number with their player ID as the account number.
- For STK Push: Safaricom sends an asynchronous callback to PesaVoucher once the transaction completes (success, failure, or cancellation) — this can take anywhere from a few seconds to a couple of minutes after the prompt is shown.
- For direct paybill payments: Safaricom notifies PesaVoucher of the C2B payment as soon as it clears, matched against the account number/player ID entered.
- PesaVoucher processes the result server-side (updates the payment record, moves merchant balances, records commission) and then forwards a normalized notification to the merchant's
callback_url.
The merchant does not talk to Safaricom directly — everything needed is in the forwarded callback described below.
A callback is sent to the merchant's registered URL exactly once per payment, as soon as PesaVoucher has processed Safaricom's result. This happens server-side, independent of whether the customer is still on any checkout page — so the merchant will be notified even if the customer closes their browser immediately after paying.
PesaVoucher sends an HTTP POST with Content-Type: application/json and header X-Callback-Type: STK-Push to the merchant's callback_url. Example payload:
{"payment_id": "b0e2f7c1-....","order_id": "ORDER-12345","merchant_request_id": "29115-34620561-1","checkout_request_id": "ws_CO_191220191020363925","status": "Success","result_code": "0","result_description": "The service request is processed successfully.","initial_amount": 500,"actual_amount": 500,"mpesa_receipt_number": "NLJ7RT61SV","transaction_date": "2026-07-15 17:20:31","phone_number": "2547XXXXXXXX","payer_phone": "2547XXXXXXXX","payer_name": "JOHN","account_reference": "ORDER-12345","voucher_code": null,"timestamp": "2026-07-15 17:20:35"}
Direct paybill deposits use the same callback structure, since Safaricom notifies PesaVoucher of these C2B payments the same way it does STK results. The one difference: because the customer initiated the payment themselves (rather than the merchant initiating an STK push against a known order), there is no order_id field — PesaVoucher has no order to attach the payment to. The account_reference field carries the account number the customer entered, which is the player ID, and that's what the merchant should use to attribute the deposit. Example:
{"payment_id": "550e8400-e29b-41d4-a716-446655440000","merchant_request_id": "MERCHANT-20251120-001","checkout_request_id": "ws_CO_20112025143022123","status": "Success","result_code": "0","result_description": "The service request is processed successfully.","initial_amount": 1250.00,"actual_amount": 1250.00,"mpesa_receipt_number": "SKL9P2M4XQ","transaction_date": "20251120143245","phone_number": "254708374149","payer_phone": "254708374149","payer_name": "JOHN DOE","account_reference": "INV-2025-0891","timestamp": "2025-11-20 14:32:50"}
| Field | Description |
|---|---|
payment_id | PesaVoucher's internal payment record ID — use this for reconciliation/support queries. |
order_id | The order reference the merchant supplied when initiating an STK push. Not present on direct paybill deposits, since those are customer-initiated with no order for PesaVoucher to attach — use account_reference (the player ID) instead to attribute those. |
merchant_request_id / checkout_request_id | Safaricom's own identifiers for the push. |
status | One of Success, Failed, Cancelled (see status table below). |
result_code / result_description | Raw Safaricom result code and human-readable description. |
initial_amount | Amount requested at push initiation (STK) or amount paid (direct paybill). |
actual_amount | Amount actually confirmed paid by Safaricom — merchants should treat this as the source of truth. |
mpesa_receipt_number | Safaricom's M-Pesa receipt code — only present on Success. |
transaction_date | Timestamp Safaricom reports the transaction completed. |
phone_number / payer_phone | Phone number the push was sent to / the number that paid. |
payer_name | Name on the M-Pesa account, if returned by Safaricom. |
voucher_code | Only populated if the payment was made through PesaVoucher's Hosted Checkout flow and a voucher was issued. For direct paybill/STK merchant integrations, this will normally be null. |
timestamp | Time PesaVoucher sent this callback (not the transaction time). |
status | result_code | Meaning |
|---|---|---|
Success | 0 | Payment completed — funds confirmed, safe to credit the player. |
Cancelled | 1032 | Customer cancelled the STK prompt or didn't respond in time (not applicable to direct paybill deposits). |
Failed | 1, 1037, 2001, or any other non-zero code | Payment did not go through — do not credit the player. |
- Verify
payment_idandorder_id/player ID match a pending or valid player account before crediting anything. - Only act on
status: "Success". TreatFailedandCancelledas no-ops other than updating the transaction's status. - Respond with a
2xxHTTP status promptly so PesaVoucher doesn't log the delivery as failed. PesaVoucher does not currently guarantee automatic retries on non-2xx responses, so a dropped/500 response on the merchant's side can result in the notification being lost — reconciliation againstpayment_idvia PesaVoucher support is the fallback if that happens. - Idempotency: it is possible (though rare) for a callback to be delivered more than once for the same
payment_id. The merchant's handler should be safe to receive the samepayment_idtwice without double-crediting. - Educate customers on the account number requirement: since direct paybill deposits rely entirely on the account number to identify the player, merchants should make it very clear in their UI/support materials that the player ID must be entered exactly as the account number — a wrong or missing player ID means the deposit can't be automatically attributed.
- STK prompts typically resolve within 5–60 seconds of being shown to the customer.
- Direct paybill payments (via M-Pesa or other banking apps) are typically confirmed within seconds of the transaction clearing on the payer's side.
- Safaricom can occasionally take longer or time out on STK; if no callback is received within ~2 minutes, the merchant can query payment status via PesaVoucher's status endpoint rather than assuming failure.
For any transaction dispute or missing callback, provide PesaVoucher with the payment_id (preferred) or checkout_request_id/account number used — these map directly to the authoritative payment record and callback log on PesaVoucher's side.