PesaBridge Express (STK Push)

PesaBridge Express is prompt-to-pay: you request money, a secure prompt appears on the customer's phone, and they confirm with their PIN. The outcome is delivered to your webhook and is queryable. Use it for checkouts, invoices, subscriptions and top-ups where you initiate the charge.

POST /partner/stk

Request body

Field

Type

Required

Description

payer_msisdn

string

yes

Customer phone, 2547xxxxxxxx (12 digits, no plus).

amount

number

yes

Amount to charge, > 0, in the account currency.

reference

string

no

Your order/invoice ref (≤ 64 chars). Shown to the customer and echoed in events.

callback_url

string

no

Overrides the app's default webhook for this one charge.

idem

string

no (recommended)

Idempotency key. A retry with the same key returns the original charge.

cURL

curl -X POST https://<your-operator>/api/bridgepay/v1/partner/stk \
  -H "Authorization: Bearer at_..." -H "Content-Type: application/json" \
  -d '{
    "payer_msisdn": "2547xxxxxxxx",
    "amount": 250,
    "reference": "INV-1001",
    "idem": "unique-key-001"
  }'

Node.js

const r = await fetch(`${base}/partner/stk`, {
  method: "POST",
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    payer_msisdn: "2547xxxxxxxx", amount: 250,
    reference: "INV-1001", idem: "unique-key-001",
  }),
});
if (!r.ok) throw new Error((await r.json()).message);
const { charge_ref, state } = (await r.json()).data;

Python

r = requests.post(f"{BASE}/partner/stk",
    headers={"Authorization": f"Bearer {token}"},
    json={"payer_msisdn": "2547xxxxxxxx", "amount": 250,
          "reference": "INV-1001", "idem": "unique-key-001"})
r.raise_for_status()
charge = r.json()["data"]   # charge_ref, state

PHP

$ch = curl_init("https://<your-operator>/api/bridgepay/v1/partner/stk");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
  CURLOPT_POSTFIELDS => json_encode([
    "payer_msisdn"=>"2547xxxxxxxx", "amount"=>250,
    "reference"=>"INV-1001", "idem"=>"unique-key-001"]),
  CURLOPT_RETURNTRANSFER => true,
]);
$charge = json_decode(curl_exec($ch), true)["data"];

Success response

{ "status":"ok", "data":{
  "charge_ref": "STK0000123",
  "state": "pending",
  "amount": 250,
  "reference": "INV-1001",
  "expiry": "2026-06-15T21:04:58Z"
} }

Field

Type

Required

Description

charge_ref

string

Unique charge id. Use it to poll status and to match webhooks.

state

string

Always pending on creation; the final state arrives via webhook.

expiry

string

When the prompt times out if the customer doesn't act.

Charge lifecycle

A charge is a small state machine, always in exactly one state:

State

Meaning

Terminal?

pending

Prompt sent; waiting for the customer.

no

authorised

Customer approved; settlement in progress.

no

completed

Confirmed; balanced ledger entries posted; money moved.

yes

declined

Customer rejected the prompt.

yes

expired

Customer didn't act before the prompt timed out.

yes

failed

A check failed (e.g. insufficient balance, limit breach).

yes

Getting the result

  • Webhook (recommended) — we POST charge.completed the instant it settles. See Webhooks.

  • Polling — call /partner/status with the charge_ref (use for reconciliation, not tight loops).

Errors

HTTP

message

Cause

Fix

404

No customer wallet for <msisdn>.

The payer has no wallet on this operator.

Use a registered customer; in sandbox use a seeded test MSISDN.

400

Amount must be positive.

amount ≤ 0.

Send a positive amount.

400

Amount exceeds the per-transaction limit…

Above the customer's control-profile cap.

Split the charge or raise the limit (operator).

401

Unauthorized.

Missing/expired token.

Refresh the token and retry once.

Always send a unique idem

If the network drops your request and you retry, the same idem returns the original charge instead of charging twice. See Idempotency.

Was this page helpful?