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, |
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, statePHP
$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 |
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.completedthe 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.

