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. charge.completed.

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 2xx quickly (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.

Was this page helpful?