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.

Base URL. Replace the host in examples with your deployment. Set BLUEPAY_BASE_URL so Safaricom → BluePay payment callbacks can reach your server (see Callbacks & your site). Current resolved base: https://bluepay.co.ke

Existing 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.

MethodPathRole
GET/api/payment_channels.phpList channels (Bearer, Basic, or dashboard session).
POST/api/stk_push.phpInitiate STK (Bearer, Basic, or session + CSRF).
POST/api/stk_push_paylink.phpSTK from a signed pay-link token (no merchant auth).
POST/api/mpesa_callback.phpSafaricom 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:

ModeWhen to useHeaders / 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).

GET https://bluepay.co.ke/api/payment_channels.php

Headers

ParameterTypeDescription
Authorization*StringBearer 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.

POST https://bluepay.co.ke/api/stk_push.php

Headers

ParameterTypeDescription
AuthorizationStringOptional. 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*Stringapplication/json

Request body

ParameterTypeDescription
channel_id*StringUUID from the dashboard. Must match the merchant for the given API key or session.
phone*StringCustomer MSISDN, e.g. 2547XXXXXXXX or 07XXXXXXXX.
amount*NumberAmount in KES (1–250000).
account_reference*StringYour order or invoice reference (max 64). Must start with your configured prefix when a prefix is set.
csrf*StringRequired 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\"}"
Apache / cPanel. If Authorization never reaches PHP, ensure RewriteRule passes HTTP_AUTHORIZATION (included in this project’s root .htaccess).

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

HeaderMeaning
X-BluePay-EventEvent name, e.g. mpesa.payment.received
X-BluePay-Signaturev1= plus lowercase hex HMAC-SHA256 of the raw JSON body using your secret API key (same value as Authorization: Bearer …).
Idempotency-KeyStable 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']
Local dev. http://127.0.0.1/… or http://localhost/… is allowed without TLS. Production callbacks should use https.
Reliability. Your script should return HTTP 2xx quickly. BluePay does not retry automatically yet; use the dashboard if you miss an event.