Webhooks
Reference
Signed events, retried for 72 hours. Without them you do not learn that a mandate was revoked, that a ceiling ran out, or — in hosted mode — what the person actually did. Polling is not a substitute: revocation is not something you can predict.
Verify the signature before you act An unverified endpoint is an open endpoint. Anyone who learns the URL can forge mandate.revoked and stop work that was authorised, or forge mandate.granted and start work that was not.
The envelope
Every delivery has the same shape: id , type , created_at , api_version , attempt and a data object that depends on the type.
Header Example What it is for
Signature and verification
HMAC-SHA256 over the string "<t>.<raw body>" , keyed with the endpoint secret whsec_… , hex-encoded in v1 . Three rules: compare in constant time, use the raw body, and reject a t older than five minutes so a captured delivery cannot be replayed at you later.
Rotate a secret in the console. Both the old and the new secret verify for 24 hours, so you can deploy without dropping an event.
Events
Hosted mode: where the session result arrives
In hosted mode the webhook is not an extra — it is the return channel. session.completed tells you the person finished; GET /sessions/{id} tells you what they produced. The redirect to your return_url is for the human, not for your business logic — a person can close the tab before it happens, and a query string is not authenticated.
Node · handler
switch (event.type) { case "session.completed": { if (await seen(event.id)) return; // dedupe on event id const s = await sidaxisGet(`/sessions/${event.data.id}`); if (s.status === "completed") await activate(s.result); if (s.status === "cancelled") await declined(s.metadata.customer_ref); break; } case "session.expired": await offerNewSession(event.data.metadata.customer_ref); break; case "mandate.revoked": await stopAgent(event.data.granted_to); // verified signature only break; }
Retries
Idempotency and ordering
Deduplicate on the event id Every event has a unique id , stable across retries. Store it and ignore an id you have already processed. A retry can arrive after the original succeeded — a slow acknowledgement on your side looks the same as a lost one on ours. Order is not guaranteed Do not infer sequence from arrival. Order by created_at , and when two events touch one mandate, let the later created_at win. A mandate.consumed that lands after a mandate.revoked does not un-revoke anything. Never treat a missing event as an answer. If a decision matters, read the resource: GET /mandates/{id} and POST /mandates/{id}/check are always authoritative.
Configuration
01 Register the endpoint in the console, per environment. An https URL, and the events you want — subscribing to all of them is the safe default. 02 Copy the secret whsec_… . It is shown once, it is per endpoint, and a sandbox secret never verifies a production event. 03 Fire a test event from the sandbox console, or force the real thing: create a mandate, revoke it, and watch mandate.revoked land. Test the unhappy paths too — a 500 from your handler should show you the retry schedule working. 04 Replay by hand from the event log — one event, or every failed event in a window — after you fix a bad deploy. The replay carries the same id , so your dedupe still applies. See also Read a session result → Idempotency on writes → Errors & refusals →
Events
| Event | Group | Fires when | What to do |
|---|---|---|---|
| mandate.granted | Mandates | A verified human issued a mandate. Sent once, after the grant is anchored. | Start the work the mandate authorises. Do not start it on your own POST /mandates response if another system of yours owns the execution. |
| mandate.consumed | Mandates | An action ran against a mandate and consumed part of its ceiling. One event per consuming check. | Reconcile your ledger against remaining. Treat remaining as authoritative, not your own subtraction. |
| mandate.at_limit | Mandates | The remaining ceiling crossed the threshold you configured. Fires before the agent is refused, not after. | Ask the human to top up or to widen the ceiling. A new ceiling needs a new grant, with a new proof of a live human. |
| mandate.revoked | Mandates | The granter revoked a mandate. Effective on the agent's next request either way. | Stop the agent. This is the event you must not accept unverified — a forged revocation stops real work, and a forged silence lets stopped work continue. |
| session.completed | Hosted | A hosted session ended with an outcome. This is how the hosted mode returns its result. | Call GET /sessions/{id} and continue your flow from the authoritative response. Never trust the redirect query string on its own. |
| session.expired | Hosted | A hosted session was never finished inside its 30-minute window. | Offer the person a new session. Nothing was billed, and the old url will not work again. |