STK Push (Accept Payments from Customers)
Trigger an instant M-Pesa payment prompt on your customer's phone. The customer enters their PIN, and funds are transferred directly to our account for float updates.
POST /stk_push
Production https://payments.pesavoucher.com/api/v1/stk_push
Sandbox https://sandbox.payments.pesavoucher.com/api/v1/stk_push
curl -X POST https://payments.pesavoucher.com/api/v1/stk_push \-H "Content-Type: application/json" \-H "X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \-H "X-API-Secret: sk_live_xxxxxxxxxxxxxxxxxxxx" \-d '{"phone_number": "254708374149","amount": 1250.00,"account_reference": "INV-2025-0891","order_id": sample_12345678","transaction_desc": "Premium Plan – Annual Subscription","metadata": {"customer_id": "CUST-8871","plan": "premium-annual"}}'
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number | string | Yes | Customer phone in 254XXXXXXXXX format (auto-converts 07xx / +254) |
amount | float | Yes | Exact amount with 2 decimal places |
account_reference | string | Yes | Your internal invoice (max 12 chars recommended) |
order_id | string | No | Your internal order ID (max 500 chars recommended) |
transaction_desc | string | Yes | Message shown to customer on their phone |
metadata | object | No | Any custom data you want attached to the transaction |
| Rule | Value |
|---|---|
| Minimum amount | 10.00 KES |
| Maximum per transaction | 150,000.00 KES |
| Decimal places | Exactly 2 |
| Examples | 250.00, 1099.50 |
Automatically accepted & converted:
254708374149→ recommended0708374149+2547083741490718 374 149(spaces ignored)
Invalid formats will return an error.
{"success": true,"message": "STK push initiated successfully","data": {"payment_id": "550e8400-e29b-41d4-a716-446655440000","merchant_request_id": "MERCHANT-20251120-001","checkout_request_id": "ws_CO_20112025143022123","customer_message": "Please check your phone and enter your PIN to complete payment","amount": 1250.00,"phone_number": "254708374149","order_id": "sample_12345678"}}
The final result is delivered via your webhook.
{"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","order_id": "sample_12345678","timestamp": "2025-11-20 14:32:50"}
| Status | Meaning | Recommended Action |
|---|---|---|
Success | Payment completed | Credit user account / fulfill order |
Failed | Payment rejected | Show error, allow retry |
Cancelled | User pressed Cancel | Allow retry |
Timeout | User didn't respond in time | Allow retry (common result_code: 1037) |
| Code | Description |
|---|---|
0 | Success |
1 | Insufficient balance |
1032 | Cancelled by user |
1037 | Timeout – no response |
2001 | Invalid credentials / account issue |
| Phone Number | Behavior |
|---|---|
254708374149 | Always succeeds instantly |
254711223344 | Always fails (insufficient funds) |
254722334455 | User cancels |
254733445566 | Timeout (no response) |
- 60 requests per minute (default)
- Burst up to 100/min
- Higher limits available on request
You're now ready to accept real-time mobile money payments though MPESA from customers!
XB STK Push (Cross-Border — Uganda & Tanzania)
Trigger an instant mobile money payment prompt for customers in Uganda (UGX) or Tanzania (TZS), powered by IntaSend's cross-border network. Same integration shape as the KES STK Push above — a customer confirms on their phone, and the final result is delivered to your webhook — but as a separate endpoint, since currency/country here is something you specify explicitly rather than something we can infer.
If you're on the KES route today and expanding into Uganda or Tanzania, this is an additive integration: keep calling /stk_push for KES traffic, and call /xb/stk_push for UGX/TZS traffic. The two are not interchangeable — sending a UGX request to /stk_push (or a KES request to /xb/stk_push) will be rejected.
POST /xb/stk_push
Production https://payments.pesavoucher.com/api/v1/xb/stk_push
curl -X POST https://payments.pesavoucher.com/api/v1/xb/stk_push \-H "Content-Type: application/json" \-H "X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \-H "X-API-Secret: sk_live_xxxxxxxxxxxxxxxxxxxx" \-d '{"currency": "UGX","phone_number": "256700123456","amount": 25000,"api_ref": "INV-2025-0891","country": "UG"}'
| Parameter | Type | Required | Description |
|---|---|---|---|
currency | string | Yes | UGX or TZS. No other values are accepted on this endpoint. |
phone_number | string | Yes | Customer phone, including country code (e.g. 256700123456, 255700123456). |
amount | number | Yes | Whole-number amount in the smallest sensible unit for the currency — see Amount Rules below. |
api_ref | string | No | Your internal reference for reconciliation. If omitted, one is generated for you and returned in the response — save it, since it's the only way to match this request to its eventual webhook. |
country | string | No | ISO country code (UG or TZ). Defaults from currency if omitted (UGX→UG, TZS→TZ) — only needed if you want to be explicit. |
Unlike /stk_push, this endpoint does not accept order_id, transaction_desc, or metadata today — those fields aren't yet part of the cross-border integration. If you need them, use api_ref to carry your own reconciliation string and let us know if metadata support is something you need — it's a straightforward addition.
| Currency | Minimum | Maximum |
|---|---|---|
| UGX | 500 | 5,000,000 |
| TZS | 500 | 5,000,000 |
These are conservative starting bounds and may be tuned as real transaction volume comes in. Unlike KES, amounts here are whole numbers, not fixed to 2 decimal places.
Send the phone number with its country code, digits only — e.g. 256700123456 for Uganda, 255700123456 for Tanzania. Unlike the KES endpoint, this one does not currently auto-normalize local formats (0700123456, +256 700 123 456, etc.) — send the full international-format digit string. We validate that the number is a plausible length (9–13 digits) on our side; final acceptance/rejection of the specific number is determined by IntaSend's own network.
{"success": true,"data": {"invoice_id": "9f2c1e6a-...","state": "PENDING","api_ref": "INV-2025-0891"}}
The final result is delivered via your webhook — see below. invoice_id is your reference for that eventual callback if you didn't supply your own api_ref.
{"id": "b7e4a210-...","invoice": {"invoice_id": "9f2c1e6a-...","state": "COMPLETE","value": "25000","api_ref": "INV-2025-0891","currency": "UGX","mpesa_reference": "...","charges": "...","net_amount": "..."}}
Note: confirm this shape against a real IntaSend sandbox/production callback before relying on it in a client integration — this reflects the nested
invoicestructure our own callback handler parses today, but hasn't been independently re-verified against IntaSend's current webhook documentation for this doc page specifically.
| State | Meaning | Recommended Action |
|---|---|---|
COMPLETE | Payment completed | Credit user account / fulfill order |
FAILED | Payment rejected | Show error, allow retry |
CANCELLED | User cancelled | Allow retry |
FAILED_VALIDATE | Number/account failed validation | Show error, allow retry with a corrected number |
PROCESSING | Still in flight — no action needed yet | Keep waiting; another callback follows |
Not yet documented for this endpoint. IntaSend's cross-border sandbox likely has its own set of test numbers with fixed simulated outcomes (mirroring how Safaricom's KES sandbox works above) — check IntaSend's own developer documentation for their current sandbox test numbers rather than assuming the KES ones above apply; they don't.
Not yet independently tuned for this endpoint — currently shares the same limits as /stk_push (60/min default, burst to 100/min). Let us know if your integration needs different limits for cross-border traffic specifically.