API reference
Base URL: {your-domain}/api/public/v1. All requests and responses are JSON. Amounts are in KES and must be whole numbers.
Authentication
Send your secret key as a bearer token. Keys are scoped to a single app and environment, and are shown only once when created or rotated.
Authorization: Bearer sk_test_9f2c4a...
Content-Type: application/json
Idempotency-Key: order_10231 # optional but recommendedReusing an Idempotency-Key within an app returns the original transaction instead of charging twice — this protects you from network and client-side retries.
POST /stkpush
Prompts the customer's handset to authorise a payment to your shortcode.
POST /api/public/v1/stkpush
{
"amount": 250,
"phone": "254708374149",
"accountReference": "ORDER-10231",
"description": "Farm inputs",
"callbackUrl": "https://yourapp.co.ke/hooks/mpesa" // optional override
}
200 OK
{
"id": "8f2c1d6a-...", // PesaBridge transaction id
"status": "pending",
"checkoutRequestId": "ws_CO_01012026...",
"merchantRequestId": "29115-34620561-1",
"customerMessage": "Success. Request accepted for processing"
}phone accepts 2547…, 07… or +2547… and is normalised for you. The final result arrives by webhook, or by polling the status endpoint.
POST /b2c
Pays a customer out from your payouts shortcode — payouts, refunds, farmer settlements.
POST /api/public/v1/b2c
{
"amount": 1500,
"phone": "254708374149",
"remarks": "Weekly milk settlement",
"occasion": "WEEK-32"
}
200 OK
{
"id": "b21f0e9c-...",
"status": "processing",
"conversationId": "AG_20260101_00007b1e...",
"originatorConversationId": "pb-b21f0e9c"
}GET /transactions/:id
Poll this if you would rather not run a webhook endpoint.
GET /api/public/v1/transactions/8f2c1d6a-...
200 OK
{
"id": "8f2c1d6a-...",
"type": "stk_push",
"status": "success", // pending | processing | success | failed | timeout
"amount": 250,
"msisdn": "254708374149",
"accountReference": "ORDER-10231",
"mpesaReceipt": "SLK7YT2QX1",
"resultCode": "0",
"resultDesc": "The service request is processed successfully.",
"createdAt": "2026-01-01T09:12:04.113Z",
"completedAt": "2026-01-01T09:12:31.480Z"
}Webhooks
Set a webhook URL on your app and we POST the same object as the status endpoint as soon as resolves the transaction. Failed deliveries retry five times with exponential backoff, and you can re-send any delivery from the dashboard.
POST https://yourapp.co.ke/hooks/mpesa
X-PesaBridge-Signature: sha256=8b1a...
X-PesaBridge-Event: transaction.updated
{ "id": "8f2c1d6a-...", "status": "success", ... }Verify the signature with your app's signing secret over the raw request body:
import { createHmac, timingSafeEqual } from "crypto";
const expected = "sha256=" + createHmac("sha256", process.env.PESABRIDGE_SIGNING_SECRET)
.update(rawBody)
.digest("hex");
const ok = expected.length === signature.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!ok) return res.status(401).end();Respond with any 2xx status within 10 seconds; anything else is retried.
Errors & rate limits
Errors use standard HTTP codes with a JSON body.
401 { "error": "invalid_api_key" }
403 { "error": "app_disabled" }
422 { "error": "validation_failed", "details": [...] }
429 { "error": "rate_limited", "retryAfter": 42 }
502 { "error": "provider_error", "message": "..." }Each secret key is limited to 120 requests per minute by default. Every request is recorded in your dashboard audit log with its status code and latency.