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.
| Rule | Why |
|---|---|
| Verify the signature before reading the body | Anyone can POST to your URL |
| Answer inside 10 seconds, work afterwards | A slow handler is retried, so it duplicates |
| Key on the event id | The same event can legitimately arrive twice |
The envelope
Every delivery has the same shape. The payload never varies by event type, only data does.
The event type is in `event`, not `type`
Reading event.type gives you undefined, and a switch on it silently falls through to your
default branch. The field is named event. The SDKs hand back this envelope unchanged, so the
same rule applies whether you parse it yourself or not.
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.
`request` does not carry your idempotency key
request.id is null, and request.idempotency_key is an idem_… value generated for the
event record. Neither reflects the Idempotency-Key header you sent. Match an event to your own
order through data.reference or data.metadata.
| Field | What it carries |
|---|---|
id | The event id, your deduplication key |
event | The type, for example payment.succeeded |
livemode | false in sandbox, true in live |
created | When the event was recorded, ISO 8601 |
api_version | The version the payload was serialised at |
pending_webhooks | How many endpoints this event is going to |
request | Identifiers of the event record itself, see the note below |
data | The resource itself |
The headers
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.0The 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
You register an endpoint
An HTTPS URL, in Konsole or through
POST /webhooks. Each endpoint gets its own signing secret. - 2
Something happens
A payment succeeds, a transfer fails, a dispute opens. Wajub records an event.
- 3
Wajub posts it
Signed, to every endpoint subscribed to that type, with a 10 second timeout.
- 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 to | Read |
|---|---|
| Receive one on your laptop | Quickstart |
| Prove it came from Wajub | Signature verification |
| Know which events exist | Event catalogue |
| Understand a duplicate | Retries and failures |
| Decide against webhooks | Webhooks 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.