BluePay Developer API
Collect via M-Pesa STK push using a channel ID from your dashboard. This reference uses the same layout as common payment API docs: endpoints, headers, JSON bodies, and example responses.
Introduction
BluePay routes STK prompts through your configured Safaricom Paybill or Till. You configure the visible channel (Paybill/Till and reference prefix) in the merchant dashboard; your integration always sends the matching channel_id on each API call.
BLUEPAY_BASE_URL so Safaricom → BluePay payment callbacks can reach your server (see Callbacks & your site). Current resolved base: https://bluepay.co.keExisting databases should run sql/migrations/002_merchant_channel_api.sql (merchant channel_id / api_key), sql/migrations/003_channels.sql (per-channel rows), sql/migrations/004_channels_account_number.sql (optional account_number per channel), sql/migrations/005_pay_link_slugs.sql (short pay URLs), and sql/migrations/006_merchant_api_credentials.sql (multiple named API credentials with Basic auth).
HTTP API (this build)
The JSON endpoints in this codebase are PHP scripts under /api/ — there is no separate versioned /v1/… route in this project. Paths like /v1/transactions on the marketing site are illustrative unless you add matching routes.
| Method | Path | Role |
|---|---|---|
GET | /api/payment_channels.php | List channels (Bearer, Basic, or dashboard session). |
POST | /api/stk_push.php | Initiate STK (Bearer, Basic, or session + CSRF). |
POST | /api/stk_push_paylink.php | STK from a signed pay-link token (no merchant auth). |
POST | /api/mpesa_callback.php | Safaricom confirmation URL (set in the M-Pesa app by the operator; merchants do not call this from their shop). |
Creating your account
Register a merchant, then open the dashboard. Under Payment channel, copy your Channel ID and Secret API key. For named Basic-auth credentials, use API Keys. Never expose the secret key in mobile apps or front-end JavaScript—call BluePay only from your server.
Authentication
Supported modes:
| Mode | When to use | Headers / body |
|---|---|---|
| Bearer (legacy) | Server-to-server; same value used for webhook / pay-link HMAC signing. | Authorization: Bearer YOUR_MERCHANT_API_KEY (legacy key on API Keys). No CSRF. |
| Basic (named credential) | Per-app keys from API Keys → Create new API access. | Authorization: Basic BASE64(api_username:api_password) (full line is shown once after creation). No CSRF. |
| Dashboard session | Testing from the browser while logged in. | Session cookie + JSON field csrf from the dashboard page (same pattern as forms). |
All JSON APIs expect Content-Type: application/json for POST requests.
Get payment channels
Retrieve the payment channel for the authenticated merchant (channel ID, short code, reference prefix).
https://bluepay.co.ke/api/payment_channels.php
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization* | String | Bearer YOUR_MERCHANT_KEY, Basic … (named credential), or omit and use a logged-in session cookie. |
Responses
200 OK
{
"ok": true,
"payment_channels": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"channel_id": "550e8400-e29b-41d4-a716-446655440000",
"channel_type": "paybill",
"short_code": "123456",
"account_reference_prefix": "BP",
"account_number": null,
"business_name": "Your Business",
"is_active": true
}
]
}
401 Unauthorized
{ "ok": false, "error": "Unauthorized" }
Initiate M-Pesa STK push
Trigger an STK prompt to a customer phone. One non-redeemable service token is debited per attempt.
https://bluepay.co.ke/api/stk_push.php
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | String | Optional. Bearer YOUR_MERCHANT_KEY or Basic BASE64(username:password) for server calls. If omitted, use a dashboard session and send csrf in the body. |
Content-Type* | String | application/json |
Request body
| Parameter | Type | Description |
|---|---|---|
channel_id* | String | UUID from the dashboard. Must match the merchant for the given API key or session. |
phone* | String | Customer MSISDN, e.g. 2547XXXXXXXX or 07XXXXXXXX. |
amount* | Number | Amount in KES (1–250000). |
account_reference* | String | Your order or invoice reference (max 64). Must start with your configured prefix when a prefix is set. |
csrf* | String | Required for dashboard session requests; omit when using Authorization: Bearer or Authorization: Basic. |
Responses
200 OK
{
"ok": true,
"stk_request_id": 1,
"checkout_request_id": "ws_CO_..."
}
400 / 405
{ "ok": false, "error": "Invalid JSON body" }
{ "ok": false, "error": "Method not allowed" }
401 / 403
{ "ok": false, "error": "Unauthorized" }
{ "ok": false, "error": "Invalid CSRF token" }
422 Unprocessable
{ "ok": false, "error": "channel_id is required" }
{ "ok": false, "error": "phone and account_reference are required" }
{ "ok": false, "error": "amount must be numeric" }
{ "ok": false, "error": "channel_id does not match this account or channel is inactive" }
402 Payment required
{ "ok": false, "error": "Insufficient service tokens." }
502 Bad gateway
{
"ok": false,
"error": "M-Pesa initiation failed (token already used for this attempt)",
"detail": null,
"stk_request_id": 1
}
When APP_ENV is not production, detail may contain a provider error message for debugging.
Code sample (cURL)
curl -X POST 'https://bluepay.co.ke/api/stk_push.php' \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"channel_id\":\"YOUR_CHANNEL_UUID\",\"phone\":\"254712345678\",\"amount\":100,\"account_reference\":\"BPINV-001\"}"
Authorization never reaches PHP, ensure RewriteRule passes HTTP_AUTHORIZATION (included in this project’s root .htaccess).Pay links (shareable)
A payment link lets you share a customer URL for a specific channel and reference, without exposing any merchant API key in the browser.
Create a pay link
In the dashboard go to Payment channels → Pay link and choose:
| Field | Meaning |
|---|---|
| Amount | Amount in KES for this link. |
| Account reference | Your order/invoice reference (must match your prefix rules if configured). |
| Validity | Expiry window (1–90 days). |
| Success URL (optional) | Where to send the customer after a successful initiation/confirmation flow on the pay page. |
| Failed URL (optional) | Where to send the customer if the payment cannot be initiated. |
Customer URL formats
The dashboard will show a short customer URL like https://bluepay.co.ke/public/payment/abc12def34… (no BluePay account required). If the slug table is missing or a row cannot be stored, it shows a single long URL like https://bluepay.co.ke/public/pay.php?t=… instead.
Set BLUEPAY_WEB_PUBLIC_PREFIX to empty in .env when the site document root is the public/ folder so links become https://bluepay.co.ke/payment/… without /public.
Authorization: Bearer or Authorization: Basic for pay-link payments.STK from a pay link (public)
The pay page calls this endpoint to initiate the STK prompt.
https://bluepay.co.ke/api/stk_push_paylink.php
Headers
| Header | Description |
|---|---|
Content-Type* | application/json |
Request body
{
"t": "FULL_TOKEN_FROM_PAY_LINK_QUERY_STRING",
"phone": "254712345678"
}
Responses
200 OK
{
"ok": true,
"stk_request_id": 1,
"checkout_request_id": "ws_CO_..."
}
400 / 405
{ "ok": false, "error": "Invalid JSON body" }
{ "ok": false, "error": "Method not allowed" }
403 Forbidden
{ "ok": false, "error": "Invalid or expired payment link" }
422 Unprocessable
{ "ok": false, "error": "t and phone are required" }
{ "ok": false, "error": "Invalid phone number" }
502 Bad gateway
{
"ok": false,
"error": "M-Pesa initiation failed (token already used for this attempt)",
"detail": null,
"stk_request_id": 1
}
It must be reachable from the same site origin as pay.php, or configure CORS if you split hosts.
Callbacks & your site
Safaricom talks to BluePay; BluePay notifies your server when a payment is confirmed. You configure one HTTPS callback URL on your side.
1. M-Pesa → BluePay (Safaricom → BluePay only)
Safaricom sends STK results to this project’s api/mpesa_callback.php on the same host as BluePay, for example https://bluepay.co.ke/api/mpesa_callback.php. Whoever runs BluePay sets BLUEPAY_BASE_URL and registers that URL in the M-Pesa app (confirmation URL). Merchants do not copy this file to their own shop or server — it is not their “Callback URL” in Account.
2. BluePay → merchant’s website (Account → Callback URL)
In Account, Callback URL is an https:// address on the merchant’s site (any path they choose, e.g. https://shop.example.com/webhooks/bluepay.php). They implement that script to read JSON, verify X-BluePay-Signature, and update orders. When a payment is confirmed and saved in BluePay, BluePay POSTs one JSON request there. This is separate from mpesa_callback.php and is not entered in Safaricom.
Headers we send
| Header | Meaning |
|---|---|
X-BluePay-Event | Event name, e.g. mpesa.payment.received |
X-BluePay-Signature | v1= plus lowercase hex HMAC-SHA256 of the raw JSON body using your secret API key (same value as Authorization: Bearer …). |
Idempotency-Key | Stable per payment (e.g. bluepay-payment-123) so you can ignore duplicates. |
JSON body (envelope)
livemode is true when APP_ENV is production. data holds the payment fields.
{
"event": "mpesa.payment.received",
"api_version": "1",
"livemode": true,
"created": "2026-04-12T12:00:00+00:00",
"data": {
"payment_id": 1,
"amount": 100.0,
"currency": "KES",
"mpesa_receipt_number": "ABC123XYZ",
"phone": "254712345678",
"account_reference": "BPINV-001",
"checkout_request_id": "ws_CO_...",
"stk_request_id": 1,
"channel_id": "550e8400-e29b-41d4-a716-446655440000",
"transaction_time": "2026-04-12 12:00:00",
"status": "success"
}
}
channel_id is your public channel UUID when the STK was tied to a channel; otherwise it may be null.
Verify in PHP (copy-paste friendly)
$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);
// handle $payload['event'] and $payload['data']
http://127.0.0.1/… or http://localhost/… is allowed without TLS. Production callbacks should use https.2xx quickly. BluePay does not retry automatically yet; use the dashboard if you miss an event.