Webhooks & signatures
Webhooks push results to you the instant they happen, so you don't have to poll. We retry with backoff until your endpoint acknowledges with a 2xx.
Event catalog
Event | When | Key payload fields |
|---|---|---|
charge.created | An Express charge was created (pending). | charge_ref, amount, reference |
charge.completed | A charge settled; money moved. | charge_ref, transaction, amount |
charge.declined | The customer rejected the prompt. | charge_ref, reference |
charge.expired | The prompt timed out. | charge_ref, reference |
c2b.confirmation | A C2B payment to your shortcode completed. | TransID, TransAmount, BillRefNumber |
Headers
Header | Value |
|---|---|
X-PesaBridge-Event | The event name, e.g. |
X-PesaBridge-Signature | HMAC-SHA256 of the raw body, hex, using your webhook signing secret. |
Verify the signature
Always verify before trusting a payload — compute the HMAC over the raw request body (not the re-serialised JSON) and compare in constant time:
Node.js
import crypto from "crypto";
function verify(rawBody, signature, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return signature && crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(signature));
}Python
import hmac, hashlib
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return bool(signature) and hmac.compare_digest(expected, signature)PHP
function verify(string $raw, ?string $sig, string $secret): bool {
return $sig && hash_equals(hash_hmac('sha256', $raw, $secret), $sig);
}Delivery & retries
We retry failed deliveries with exponential backoff until you return a
2xx(several attempts over ~24h).Respond
2xxquickly (under a few seconds); do slow work after acknowledging.Be idempotent — key on
charge_ref/TransID; a redelivered event must be a no-op.Order is not guaranteed; if you need certainty, reconcile with status.
Protect against replay: reject events whose timestamp is older than a few minutes.
Test locally
Point your callback at a tunnel (e.g. ngrok) and trigger events from the API Console / C2B simulator to see real signed deliveries against your code.

