Server-side M-PESA payments
A clear path from request to verified result.
Use this five-minute path to initiate an M-PESA payment, keep your credentials on the server, and reconcile the terminal result.
Five-minute path
- Create a Sandbox API key in the owner dashboard and keep it on your server.
- Send
POST /api/v1/payments/stkwith a uniqueIdempotency-Key. - Treat HTTP
202as accepted for processing, then store the returned payment reference. - Poll the status endpoint or verify signed webhook events until a terminal result.
- On an uncertain response, reuse the same idempotency key; do not create a new attempt.
Authentication
Call payment endpoints only from a trusted server. Use a placeholder such as bcp_test_REPLACE_ME while integrating. Never place keys in browser code, logs, or generated client snippets.
curl -X POST https://api.example.test/api/v1/payments/stk \
-H 'Authorization: Bearer bcp_test_REPLACE_ME' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order_123_attempt_1' \
-d '{"phone_number":"2547XXXXXXX","amount":100,"external_reference":"order_123"}'Use these placeholders on your server; never put a key in browser code.
Asynchronous payment status
A 202 Accepted response means initiation was accepted. It does not mean the customer paid. Reconcile through GET /api/v1/payments/{payment_id} or a verified webhook before marking an order paid.
Signed webhooks
Read the raw request body before parsing JSON. Require the webhook ID, Unix-seconds timestamp, v1=<hex> signature, and secret version; reject timestamps outside 900 seconds. Compare v1=<hex(HMAC-SHA256(secret_for_version, timestamp + "." + raw_body))> in constant time, persist the event ID before fulfillment, and return 2xx only after durable acceptance.
Safe retries and errors
Keep one stable key per logical attempt. Respect Retry-After on 429 responses and back off on 503. An ambiguous payment remains pending until reconciliation or owner instruction.
Public operation map
These are the public operations in the validated contract. Owner management and internal routes are intentionally excluded.
POST /api/v1/payments/stkInitiate an M-PESA STK PushGET /api/v1/paymentsSearch paymentsGET /api/v1/payments/{payment_id}Get a paymentGET /api/public/v1/payment-links/{slug}Resolve an active hosted payment linkPOST /api/public/v1/payment-links/{slug}/payments/stkInitiate an STK Push from an active payment linkGET /api/public/v1/payment-links/{slug}/payments/{payment_id}Poll a payment created through a hosted linkPOST /api/public/v1/waitlistSubmit a coming-soon waitlist request