API reference
M-Pesa STK, channels, status & webhooks. Server-side only — never expose API secrets in the browser.
Quick start
API Keys → secret or Basic credential. Channels → channel_id for STK.
Integration checklist
Pick one auth method (Bearer or Basic). All calls from your server.
- Authenticate — Bearer + API secret, or Basic
api_username:api_password. - Channel — copy
channel_id(UUID, not a password). - STK —
channel_id,phone,amount, optionalaccount_reference. - Webhooks — HMAC with API secret on raw body. Callbacks.
https://bluepay.co.ke. Set BLUEPAY_BASE_URL so Safaricom can reach /api/mpesa_callback.php.- Channels —
GETorPOST https://bluepay.co.ke/api/payment_channels.php— Bearer/Basic header recommended. - STK —
POST https://bluepay.co.ke/api/stk_push.phporhttps://bluepay.co.ke/api/v2/payments.php—channel_id,phone,amount, optionalaccount_reference, optionalcallback_url(migration010). - Status —
POST https://bluepay.co.ke/api/payment_status.php—checkout_request_idoraccount_reference. Treatpaid: trueas settled.
Optional: Account → Callback URL for webhooks. Poll status as backup.
All endpoints
| Method | Path | Purpose |
|---|---|---|
GET / POST | /api/payment_channels.php | List merchant channels. |
GET / POST | /api/payment_status.php | Poll STK / receipt state. |
POST | /api/stk_push.php · /api/v2/payments.php | Start STK (identical). |
POST | /api/stk_push_paylink.php | STK with pay-link token t. |
POST | /api/stk_push_paydefault.php | STK for default/channel pay links. |
POST | /api/mpesa_callback.php | Safaricom → BluePay only. |
Authentication
Pick one method per request. channel_id is not authentication. POST bodies need Content-Type: application/json.
Authorization: Bearer <API secret>. Webhook HMAC always uses the API secret, not Basic credentials.| Mode | How | Notes |
|---|---|---|
| Bearer | Authorization: Bearer + API secret | Webhook HMAC uses this key. |
| Basic | Authorization: Basic + Base64(api_username:api_password) | Named credential from API Keys. |
| JSON body | api_username + api_password or api_key in JSON | Legacy; prefer headers. May return warnings. |
| Session | Dashboard cookie + csrf | Browser only — not for server integrations. |
Payment channels
https://bluepay.co.ke/api/payment_channels.php
POST: JSON auth fields in body. GET: Bearer/Basic header or session.
Example response (200 / 401)
{
"ok": true,
"payment_channels": [{ "channel_id": "550e8400-e29b-41d4-a716-446655440000", "channel_type": "paybill", "short_code": "123456", "is_active": true }]
}
401
{ "ok": false, "error": "Unauthorized", "message": "Authenticate with Authorization: Bearer … or Basic …" }
Payment status
Call from your backend with the same auth as STK. Pass one lookup key.
https://bluepay.co.ke/api/payment_status.php?checkout_request_id=…
https://bluepay.co.ke/api/payment_status.php
| Field | Notes |
|---|---|
checkout_request_id | Best key — from STK response. |
account_reference | Your payment / invoice id. Latest STK for that ref. Legacy alias: external_reference, account_id (not your BP-… merchant id). |
stk_request_id | Integer from STK response. |
paid = settled · found = row exists · order.status = pending|completed|failed · payment.pending_confirmation while receipt is RCHK-….
Example responses
{
"ok": true,
"success": true,
"found": true,
"paid": true,
"failed": false,
"stk_status": "SUCCESS",
"stk_request_id": 42,
"checkout_request_id": "ws_CO_...",
"account_reference": "BPLINK-A8A25317",
"external_reference": "BPLINK-A8A25317",
"amount": 100,
"phone": "254742783614",
"payment": {
"id": 10,
"mpesa_receipt_number": "ABC12XYZ",
"amount": 100,
"phone": "254742783614",
"transaction_time": "2026-04-19 09:50:00",
"pending_confirmation": false
},
"wallet_topup": null,
"created_at": "2026-04-19 09:48:00",
"order": {
"status": "completed",
"checkout_request_id": "ws_CO_...",
"external_reference": "BPLINK-A8A25317",
"amount": 100,
"phone": "254742783614",
"stk_request_id": 42
}
}
Not found (still 200)
{
"ok": true,
"success": true,
"found": false,
"paid": false,
"failed": false,
"stk_status": null,
"message": "No matching STK for this merchant yet",
"payment": null
}
422
{ "ok": false, "success": false, "error": "Provide checkout_request_id, account_reference (recommended), external_reference, stk_request_id, or deprecated account_id" }
STK push
Requires prepaid service-token balance (see 402 / INSUFFICIENT_PREPAID). Fee on successful payment.
https://bluepay.co.ke/api/stk_push.php
Alias https://bluepay.co.ke/api/v2/payments.php · Content-Type: application/json* · Bearer/Basic header recommended.
Body
| Field | Required | Notes |
|---|---|---|
channel_id | Yes | Alias payment_channel_id. Dashboard channel UUID. |
phone | Yes | Alias phone_number. Kenya MSISDN. |
amount | Yes | KES 1–250000. |
account_reference | No | Payment reference (invoice id). Not your BluePay BP-… account id. Max 64; prefix rules apply. Omitted → BluePay generates ref. Legacy aliases: external_reference, account_id. |
| Auth | Yes | Authorization: Bearer or Basic header (recommended). Legacy: api_username / api_password or api_key in JSON. |
csrf | Session only | With dashboard cookie, no API key in body. |
provider | No | m-pesa or omit. |
callback_url | No | Per-STK webhook override — migration 010. Safaricom still posts to BluePay only. |
Request & success examples
{
"channel_id": "550e8400-e29b-41d4-a716-446655440000",
"phone": "254712345678",
"amount": 100,
"account_reference": "BPINV-2048"
}
Use Bearer or Basic in headers (preferred) or legacy api_username / api_password in JSON.
200
{ "ok": true, "stk_request_id": 1, "checkout_request_id": "ws_CO_...", "account_reference": "BP-LINK-A1B2C3D4" }
Error responses (400–502)
400 / 405
{ "ok": false, "success": false, "error": "Invalid JSON body", "message": "Invalid JSON body" }
{ "ok": false, "success": false, "error": "Method not allowed", "message": "Method not allowed" }
401 / 403
{ "ok": false, "success": false, "error": "Unauthorized", "message": "Authenticate with Authorization: Bearer … or Basic …", "hint": "See /public/docs.php#authentication" }
{ "ok": false, "success": false, "error": "Invalid CSRF token", "message": "Invalid CSRF token" }
422
{ "ok": false, "success": false, "error": "…", "message": "…" }
402
Branch on error_code: INSUFFICIENT_PREPAID + required_kes / balance_kes.
{
"ok": false,
"success": false,
"error": "402 Insufficient service tokens",
"message": "402 Insufficient service tokens",
"error_code": "INSUFFICIENT_PREPAID",
"required_kes": 1,
"balance_kes": 0
}
error_code (pay-default / validation)
| Code | When |
|---|---|
INSUFFICIENT_PREPAID | 402 — top up service tokens. |
REFERENCE_PREFIX_MISMATCH | Pay-default — ref prefix vs channel required_prefix. |
INVALID_PHONE | MSISDN not normalizable to 254… |
CHANNEL_NOT_AVAILABLE | Channel inactive or merchant inactive. |
ACCOUNT_REFERENCE_INVALID | Ref failed M-Pesa rules. |
STK_VALIDATION_FAILED | Pay-default — missing fields. |
502
{ "ok": false, "success": false, "error": "…", "message": "…", "detail": null, "stk_request_id": 1 }
Non-production: detail may include provider message.
Code samples (cURL, PHP, Node)
curl -sS -X POST 'https://bluepay.co.ke/api/stk_push.php' \
-H "Content-Type: application/json" \
-d '{"api_username":"…","api_password":"…","channel_id":"…","phone":"2547…","amount":100}'
<?php
$body = [
'api_username' => '…',
'api_password' => '…',
'channel_id' => '…',
'phone' => '2547…',
'amount' => 100,
];
$ch = curl_init('https://bluepay.co.ke/api/stk_push.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
const res = await fetch("https://bluepay.co.ke/api/stk_push.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_username: "…",
api_password: "…",
channel_id: "…",
phone: "2547…",
amount: 100,
}),
});
console.log(res.status, await res.json());
Authorization to PHP if you use Bearer/Basic headers.Pay links
Dashboard Payment channels → Pay link. Customer pays via signed URL — no API key in the browser.
STK endpoints for pay links
URLs: https://bluepay.co.ke/public/payment/… or https://bluepay.co.ke/public/pay.php?t=…
https://bluepay.co.ke/api/stk_push_paylink.phpFixed amount: {"t":"…","phone":"2547…"}
https://bluepay.co.ke/api/stk_push_paydefault.phpDefault/channel: {"t":"…","phone":"2547…","channel_id":"UUID","amount":100} — see STK errors.
SMS API
Send transactional SMS from your server using the BluePay sender ID (FULL_CIRCLE on this platform). You do not pass sender_name — BluePay applies the registered sender ID and returns it in sender_name. Delivery is to Safaricom numbers only. Use a unique reference per message for idempotency.
https://bluepay.co.ke/api/v1/sms/sendAuth: same as STK — API Keys secret as Bearer or Basic. Active merchant account required. Not for browser use.
Body
| Field | Required | Notes |
|---|---|---|
phone | Yes | Safaricom MSISDN only — 071x/072x/074x/079x or 2547…; newer 011x numbers as 2541…. Alias mobile. |
message | Yes | Max 480 chars. Alias text. |
reference | Yes | Your idempotency key (max 64; A-Za-z0-9._-). Re-posting a successful reference returns the same result. |
{
"phone": "254712345678",
"message": "Your order #8821 is ready for pickup.",
"reference": "ORDER-8821-SMS"
}
200 — success
{
"ok": true,
"success": true,
"reference": "ORDER-8821-SMS",
"phone": "254712345678",
"status": "SENT",
"fee_kes": 2.5,
"segments": 1,
"message_id": 12345,
"sender_name": "FULL_CIRCLE",
"balance_kes": 97.5,
"sms_id": 1
}
Example (cURL)
curl -sS -X POST 'https://bluepay.co.ke/api/v1/sms/send' \
-H "Authorization: Bearer YOUR_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"phone":"254712345678","message":"Your order is ready.","reference":"ORDER-8821-SMS"}'
View sends in the dashboard under SMS. Merchants need sufficient service tokens before send; fee is debited on success.
error_code values
| HTTP | error_code | When |
|---|---|---|
| 402 | INSUFFICIENT_PREPAID | Service tokens below SMS fee (required_kes, balance_kes) |
| 409 | REFERENCE_IN_USE | Same reference still PENDING |
| 422 | SMS_VALIDATION_FAILED | Invalid phone (non-Safaricom or bad format), message, or reference |
| 502 | SMS_PROVIDER_REJECTED | SMS gateway rejected the message |
| 503 | SMS_NOT_CONFIGURED | Platform SMS gateway not configured |
B2C payouts
Send money to a customer’s M-Pesa number from your B2C wallet. Requires an active merchant account (KYC verified). Top up from the dashboard wallet card or via STK (B2CTOPUP- references). View sends on Payouts.
Two balances: B2C wallet = payout principal. Service tokens = platform fee per payout (debited on initiation, not refunded if M-Pesa fails). On failure, the B2C wallet amount is automatically reversed.
https://bluepay.co.ke/api/v1/b2c/payoutsAuth: same as STK (Authorization: Bearer YOUR_API_KEY or Basic credential).
{
"amount": 500,
"phone": "0712345678",
"reference": "WD-2024-001",
"description": "Commission payout",
"callbackUrl": "https://yourserver.com/webhooks/b2c"
}
Amount KES 10–1,000,000 per recipient. Platform fee bands are on Pricing (flat fee per band; 2% of amount from KES 250,000 upward). Responses include transaction_fee (service tokens). Terminal status arrives via callbackUrl or your account callback URL.
https://bluepay.co.ke/api/v1/b2c/wallet/balanceReturns b2c_wallet.balance_kes and service_wallet.balance_kes.
https://bluepay.co.ke/api/v1/b2c/payout_status?payout_id={uuid}Poll payout status after POST /b2c/payouts. Response data.payout includes status (PENDING, SUCCESS, FAILED, REVERSED), amounts, and M-Pesa receipt when available.
Dashboard (session): merchants top up or withdraw B2C float from the wallet card via /api/b2c_wallet_topup.php and /api/b2c_wallet_withdraw.php (browser login + CSRF — not for server integrations).
B2C error_code values
Same envelope as STK: ok, success, error, message, error_code. Balance errors include required_kes and balance_kes.
| HTTP | error_code | When |
|---|---|---|
| 402 | INSUFFICIENT_PREPAID | Service tokens too low for B2C fee (transaction_fee_kes may be included) |
| 403 | MERCHANT_INACTIVE | Merchant account not active |
| 403 | B2C_DISABLED | B2C blocked for this account (inactive or admin override) |
| 409 | INSUFFICIENT_B2C_BALANCE | B2C float wallet too low for payout amount |
| 409 | REFERENCE_IN_USE | reference already used (pending or successful payout) |
| 422 | INVALID_PHONE | MSISDN not normalizable to 254… (same as STK) |
| 422 | B2C_VALIDATION_FAILED | Invalid amount, reference, callback URL, etc. |
| 503 | B2C_UNAVAILABLE | Schema or M-Pesa B2C credentials missing |
| 502 | B2C_GATEWAY_ERROR | M-Pesa or unexpected failure after accept |
402 — insufficient service tokens (B2C fee)
{
"ok": false,
"success": false,
"error": "402 Insufficient service tokens",
"message": "402 Insufficient service tokens",
"error_code": "INSUFFICIENT_PREPAID",
"required_kes": 25,
"balance_kes": 10,
"transaction_fee_kes": 25
}
409 — insufficient B2C float
{
"ok": false,
"success": false,
"error": "409 Insufficient B2C wallet balance",
"message": "409 Insufficient B2C wallet balance",
"error_code": "INSUFFICIENT_B2C_BALANCE",
"required_kes": 500,
"balance_kes": 120
}
B2C webhook events
mpesa.b2c.success · mpesa.b2c.failed · mpesa.b2c_wallet_topup.received (after B2C wallet STK top-up).
{
"event": "mpesa.b2c.success",
"data": {
"id": "uuid",
"type": "B2C",
"status": "SUCCESS",
"amount": 500,
"phone_number": "254712345678",
"external_reference": "WD-2024-001",
"mpesa_receipt": "RKL1XXXXX0",
"transaction_fee": 25.00
}
}
Webhooks
Account → Callback URL — HTTPS (or http:// localhost). BluePay POSTs JSON on success/failure. Optional per-STK override: callback_url on STK (migration 010).
| Header | Value |
|---|---|
X-BluePay-Event | mpesa.payment.received · mpesa.payment.failed · mpesa.wallet_topup.received · mpesa.b2c.success · mpesa.b2c.failed · mpesa.b2c_wallet_topup.received |
X-BluePay-Signature | v1= + hex HMAC-SHA256(raw body, API secret from API Keys). Always this key — not Basic credentials. HTTP 401 on your endpoint usually means signature mismatch. |
Idempotency-Key | Dedupe key per delivery. |
Payload example & verification code
livemode mirrors production. Key data fields: account_reference, checkout_request_id, mpesa_receipt_number, amount, phone, status.
{
"event": "mpesa.payment.received",
"api_version": "1",
"livemode": true,
"data": {
"payment_id": 1,
"amount": 100,
"mpesa_receipt_number": "ABC123XYZ",
"account_reference": "BPINV-001",
"checkout_request_id": "ws_CO_...",
"status": "success"
}
}
Failures: mpesa.payment.failed. Wallet top-ups: mpesa.wallet_topup.received.
$raw = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_BLUEPAY_SIGNATURE'] ?? '';
if (!preg_match('/^v1=([a-f0-9]{64})$/', $sigHeader, $m)) { http_response_code(400); exit; }
$expected = hash_hmac('sha256', $raw, 'YOUR_SECRET_API_KEY');
if (!hash_equals($expected, $m[1])) { http_response_code(401); exit; }
$payload = json_decode($raw, true);
const raw = req.body;
const sig = req.get("X-BluePay-Signature") || "";
const m = /^v1=([a-f0-9]{64})$/.exec(sig);
const expected = crypto.createHmac("sha256", secret).update(raw).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(m[1], "hex")))
return res.sendStatus(401);
2xx quickly. No automatic webhook retries — use payment status as backup.