Event catalogue
Generated from the same module the emitter is typed against, so it cannot list an event nothing sends. Your dashboard serves the same list, with a sample payload for each.
| type | When it fires | State |
|---|---|---|
| payment_intent.created | A payment was created and is waiting to be confirmed. | |
| payment_intent.processing | The payment was routed to a provider and is under way. | |
| payment_intent.requires_action | The customer has to approve a prompt, enter a code or finish a redirect. | |
| payment_intent.succeeded | The funds are confirmed. Fulfil the order here. | |
| payment_intent.failed | Terminal failure, with a canonical code saying why. | |
| payment_intent.canceled | Cancelled before it completed. | |
| payment_intent.expired | The customer never completed it in time. | |
| charge.succeeded | One attempt took the money. | |
| charge.failed | One attempt was declined. The payment may still succeed elsewhere. | |
| charge.refunded | A reversal settled. | Not yet emitted |
| disbursement.succeeded | A payout to a third party settled. | Not yet emitted |
| payout.paid | A withdrawal to your own account settled. | Not yet emitted |
| balance.updated | Your available balance changed. | Not yet emitted |
| dispute.opened | A payer’s bank opened a chargeback. | Not yet emitted |
The payload
One envelope for every event: an id to deduplicate on, a type to branch on, the environment, and the object itself under data.object. Amounts are decimal strings and dates are epoch milliseconds, exactly as everywhere else.
{
"id": "wd_01JAY7BQ2M4X8ND6VKQ0R3PZ5W",
"object": "event",
"type": "payment_intent.succeeded",
"created": 1785869000,
"environment": "TEST",
"data": {
"object": {
"id": "pi_01JAY2K5RQ9M3W7ZB8XF4TC6VD",
"object": "payment_intent",
"status": "SUCCEEDED",
"amount": "500000",
"amountCaptured": "500000",
"currency": "XAF",
"country": "CM",
"methodType": "MOBILE_MONEY",
"provider": "SIMULATOR",
"flowKind": "DEVICE_PUSH",
"reference": "order-4471",
"created": 1785869000000
}
}
}Verifying the signature
The IdealBox-Signature header carries a timestamp and an HMAC over "{timestamp}.{raw body}". Verify it before you trust a single field.
# IdealBox-Signature: t=1785869000,v1=5257a869e7…
#
# signed_payload = "{t}.{raw_body}"
# expected = HMAC-SHA256(endpoint_secret, signed_payload)
#
# Compare in constant time, and reject anything where |now - t| is more than
# five minutes. Without the timestamp in the signed string, a captured request
# would verify forever.
#
# During a secret rotation BOTH the new and the previous secret sign, so you
# can deploy the new one without a synchronised cutover.Four rules that are not optional
Every integration that has ever gone wrong here went wrong on one of these.
- Fulfil on payment_intent.succeeded from the webhook, never on the HTTP response to confirm. A device-push payment is not complete when that call returns.
- Expect duplicates. Delivery is at-least-once — deduplicate on the envelope id.
- Expect events out of order. Compare against your own stored state rather than assuming a sequence.
- Verify the signature against the raw bytes. A parsed and re-serialised body will not match the HMAC, and that mistake is invisible until production.
- Answer 2xx quickly and do your work afterwards. Anything else is counted as a failure, and twenty consecutive failures disables the endpoint.
Replaying an event
Any past event can be sent again from Developers → Events, either to every subscribed endpoint or to one that missed it.
{
"id": "wd_01JB0C4XN2Q8T5R7YH3MF6PW9K",
"object": "event",
"type": "payment_intent.succeeded",
"created": 1785872600,
"environment": "TEST",
"replay": {
"of": "wd_01JAY7BQ2M4X8ND6VKQ0R3PZ5W",
"requestedAt": 1785872600000
},
"data": { "object": { "…": "unchanged" } }
}