Skip to content

Webhooks

How Wajub tells your server what happened, and how to trust it.

A mobile money payment is not finished when your call returns. The payer still has to type their PIN, the operator still has to answer, and that can take seconds or minutes. A webhook is how you learn the outcome without asking for it.

Three things make an integration correct here, and the rest of this section is the detail behind them.

RuleWhy
Verify the signature before reading the bodyAnyone can POST to your URL
Answer inside 10 seconds, work afterwardsA slow handler is retried, so it duplicates
Key on the event idThe same event can legitimately arrive twice

The envelope

Every delivery has the same shape. The payload never varies by event type, only data does.

A payment.succeeded delivery
{
"id": "evt_test_aio5DpN577tNU2vOxdmuZGhT",
"event": "payment.succeeded",
"livemode": false,
"created": "2026-05-24T10:21:09+00:00",
"api_version": "2026-09-01",
"pending_webhooks": 1,
"request": {
"id": null,
"idempotency_key": "idem_kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0"
},
"data": {
"id": "trx_CSUGajfv9xh0XQ5wu2lx",
"reference": "order-4172",
"amount": 25000,
"amount_paid": 25000,
"currency": "XAF",
"status": "succeeded",
"channel": "cm.mtn",
"customer": {
"id": "cus_test_4tRb3nP8sZcXvK2mQ9wL",
"email": "amina@example.com"
}
}
}

data is the full resource, byte for byte what GET /payments/{id} returns under transaction. You never need a second call to learn what happened, though making one is a reasonable way to re-check a payment you consider high value.

FieldWhat it carries
idThe event id, your deduplication key
eventThe type, for example payment.succeeded
livemodefalse in sandbox, true in live
createdWhen the event was recorded, ISO 8601
api_versionThe version the payload was serialised at
pending_webhooksHow many endpoints this event is going to
requestIdentifiers of the event record itself, see the note below
dataThe resource itself

The headers

What arrives with the body
X-Wajub-Signature: v1=8f3c1b0e4a7d...
X-Wajub-Timestamp: 1748081269
X-Wajub-Event: payment.succeeded
X-Wajub-Delivery-Id: whd_7Yh2MpL4tRb3nP8sZcXv
Content-Type: application/json
User-Agent: Halo/1.0

The first two are what you verify against. X-Wajub-Event lets you route before parsing, and X-Wajub-Delivery-Id identifies this attempt rather than the event, so it changes on every retry while the event id does not.

What happens end to end

  1. 1

    You register an endpoint

    An HTTPS URL, in Konsole or through POST /webhooks. Each endpoint gets its own signing secret.

  2. 2

    Something happens

    A payment succeeds, a transfer fails, a dispute opens. Wajub records an event.

  3. 3

    Wajub posts it

    Signed, to every endpoint subscribed to that type, with a 10 second timeout.

  4. 4

    You answer 2xx

    Anything else, or no answer in time, and the delivery is attempted five times over sixteen minutes.

Where to go next

You want toRead
Receive one on your laptopQuickstart
Prove it came from WajubSignature verification
Know which events existEvent catalogue
Understand a duplicateRetries and failures
Decide against webhooksWebhooks or polling

You do not need a public URL to start

wajub listen --forward-to http://localhost:3000/webhooks streams live events straight to your machine, signed the same way as production. The quickstart starts there.

What did you think of this content?