BluePay API
Ask AI

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. Channelschannel_id for STK.

Integration checklist

Pick one auth method (Bearer or Basic). All calls from your server.

  1. Authenticate — Bearer + API secret, or Basic api_username:api_password.
  2. Channel — copy channel_id (UUID, not a password).
  3. STKchannel_id, phone, amount, optional account_reference.
  4. Webhooks — HMAC with API secret on raw body. Callbacks.
Base URLhttps://bluepay.co.ke. Set BLUEPAY_BASE_URL so Safaricom can reach /api/mpesa_callback.php.
  1. ChannelsGET or POST https://bluepay.co.ke/api/payment_channels.php — Bearer/Basic header recommended.
  2. STKPOST https://bluepay.co.ke/api/stk_push.php or https://bluepay.co.ke/api/v2/payments.phpchannel_id, phone, amount, optional account_reference, optional callback_url (migration 010).
  3. StatusPOST https://bluepay.co.ke/api/payment_status.phpcheckout_request_id or account_reference. Treat paid: true as settled.

Optional: Account → Callback URL for webhooks. Poll status as backup.

All endpoints

MethodPathPurpose
GET / POST/api/payment_channels.phpList merchant channels.
GET / POST/api/payment_status.phpPoll STK / receipt state.
POST/api/stk_push.php · /api/v2/payments.phpStart STK (identical).
POST/api/stk_push_paylink.phpSTK with pay-link token t.
POST/api/stk_push_paydefault.phpSTK for default/channel pay links.
POST/api/mpesa_callback.phpSafaricom → BluePay only.

Authentication

Pick one method per request. channel_id is not authentication. POST bodies need Content-Type: application/json.

RecommendedAuthorization: Bearer <API secret>. Webhook HMAC always uses the API secret, not Basic credentials.
ModeHowNotes
BearerAuthorization: Bearer + API secretWebhook HMAC uses this key.
BasicAuthorization: Basic + Base64(api_username:api_password)Named credential from API Keys.
JSON bodyapi_username + api_password or api_key in JSONLegacy; prefer headers. May return warnings.
SessionDashboard cookie + csrfBrowser only — not for server integrations.

Payment channels

GET POST 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.

GET https://bluepay.co.ke/api/payment_status.php?checkout_request_id=…
POST https://bluepay.co.ke/api/payment_status.php
FieldNotes
checkout_request_idBest key — from STK response.
account_referenceYour payment / invoice id. Latest STK for that ref. Legacy alias: external_reference, account_id (not your BP-… merchant id).
stk_request_idInteger 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.

POST 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

FieldRequiredNotes
channel_idYesAlias payment_channel_id. Dashboard channel UUID.
phoneYesAlias phone_number. Kenya MSISDN.
amountYesKES 1–250000.
account_referenceNoPayment reference (invoice id). Not your BluePay BP-… account id. Max 64; prefix rules apply. Omitted → BluePay generates ref. Legacy aliases: external_reference, account_id.
AuthYesAuthorization: Bearer or Basic header (recommended). Legacy: api_username / api_password or api_key in JSON.
csrfSession onlyWith dashboard cookie, no API key in body.
providerNom-pesa or omit.
callback_urlNoPer-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)

CodeWhen
INSUFFICIENT_PREPAID402 — top up service tokens.
REFERENCE_PREFIX_MISMATCHPay-default — ref prefix vs channel required_prefix.
INVALID_PHONEMSISDN not normalizable to 254…
CHANNEL_NOT_AVAILABLEChannel inactive or merchant inactive.
ACCOUNT_REFERENCE_INVALIDRef failed M-Pesa rules.
STK_VALIDATION_FAILEDPay-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());
cPanel / Apache — Forward Authorization to PHP if you use Bearer/Basic headers.

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.

POSThttps://bluepay.co.ke/api/v1/sms/send

Auth: same as STK — API Keys secret as Bearer or Basic. Active merchant account required. Not for browser use.

Body

FieldRequiredNotes
phoneYesSafaricom MSISDN only — 071x/072x/074x/079x or 2547…; newer 011x numbers as 2541…. Alias mobile.
messageYesMax 480 chars. Alias text.
referenceYesYour 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

HTTPerror_codeWhen
402INSUFFICIENT_PREPAIDService tokens below SMS fee (required_kes, balance_kes)
409REFERENCE_IN_USESame reference still PENDING
422SMS_VALIDATION_FAILEDInvalid phone (non-Safaricom or bad format), message, or reference
502SMS_PROVIDER_REJECTEDSMS gateway rejected the message
503SMS_NOT_CONFIGUREDPlatform 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.

POSThttps://bluepay.co.ke/api/v1/b2c/payouts

Auth: 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.

GEThttps://bluepay.co.ke/api/v1/b2c/wallet/balance

Returns b2c_wallet.balance_kes and service_wallet.balance_kes.

GEThttps://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.

HTTPerror_codeWhen
402INSUFFICIENT_PREPAIDService tokens too low for B2C fee (transaction_fee_kes may be included)
403MERCHANT_INACTIVEMerchant account not active
403B2C_DISABLEDB2C blocked for this account (inactive or admin override)
409INSUFFICIENT_B2C_BALANCEB2C float wallet too low for payout amount
409REFERENCE_IN_USEreference already used (pending or successful payout)
422INVALID_PHONEMSISDN not normalizable to 254… (same as STK)
422B2C_VALIDATION_FAILEDInvalid amount, reference, callback URL, etc.
503B2C_UNAVAILABLESchema or M-Pesa B2C credentials missing
502B2C_GATEWAY_ERRORM-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).

HeaderValue
X-BluePay-Eventmpesa.payment.received · mpesa.payment.failed · mpesa.wallet_topup.received · mpesa.b2c.success · mpesa.b2c.failed · mpesa.b2c_wallet_topup.received
X-BluePay-Signaturev1= + 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-KeyDedupe 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);
Return 2xx quickly. No automatic webhook retries — use payment status as backup.