POWERED BY
SafaricomM-PESA

PesaBridge

UNIFIED PAYMENT GATEWAY

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 recommended

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