Outbound webhooks
Being told when something changes, how to verify the signature, and how to recover the events you missed.
5 min read
On this page
Register an endpoint and Evident POSTs to it when something happens, rather than you polling for it. Settings → Webhooks, or /outbound-webhooks on the API.
Events
| Event | Fires when |
|---|---|
review.created | A review is submitted |
review.approved | A review is published |
review.rejected | A review is rejected |
review.updated | A review is edited |
review.video_ready | An attached video finishes transcoding and is playable |
order.placed | An order is first seen |
order.completed | An order reaches a completed or shipped status |
gallery.submitted | A customer submits a photo to a gallery |
gallery.approved | A merchant publishes a submitted item |
redemption.created | A customer spends points |
redemption.used | A redemption is used |
redemption.expired | A redemption lapses unused |
Subscribe per event. A webhook subscribes to the events you choose, not to everything.
Two of these are worth calling out:
review.video_readyexists becausereview.createdcan arrive while the video is still transcoding. If you re-render a review with playback, listen for this rather than retrying onreview.created.redemption.createdcarries the discount code and afulfillment_status. On a headless store, or for any reward the platform cannot express, that arrives asunsupported— your signal that you must issue the discount yourself.
The payload
{
"event_id": "6b1e...",
"event": "review.approved",
"timestamp": "2026-09-02T10:14:22.000Z",
"data": { }
}
event_id is stable per logical event. Deduplicate on it.
Headers
| Header | Contents |
|---|---|
x-evident-signature | HMAC-SHA256 of the raw body, hex, keyed with your webhook secret |
x-evident-event | The event name, so you can route before parsing |
x-evident-webhook-id | Unique per HTTP attempt — not the same as event_id |
Verifying the signature
Compute the HMAC over the raw request body, before any JSON parsing. Frameworks that parse and re-serialize will change the bytes and break verification.
import crypto from 'node:crypto';
function verify(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
Compare in constant time, as above. A plain === leaks timing information.
Reject anything that does not verify. The endpoint is public by necessity, and the signature is the only thing separating a real event from anyone who guesses your URL.
Delivery, honestly
Delivery is fire-and-forget with a 15-second timeout. There is no automatic retry queue today. A non-2xx response or a timeout is recorded against the webhook — you can see the last error and last fired time on the subscription — but it is not re-attempted.
So: do not treat webhooks as your only path. Acknowledge fast (queue the work, return 200 immediately) and reconcile periodically.
Recovering what you missed
There is an event log for exactly this. It replays a time-ordered stream of webhook-shaped events for a store, reconstructed from the underlying rows rather than from delivery attempts — so it covers events that were never delivered at all, including ones from before your subscription existed.
It is cursor-paginated: pass a since timestamp, and the response carries a next_cursor when more remain. Events are ordered by timestamp then ID, so the stream is stable when several share a timestamp.
A daily reconciliation against this log is a few lines of code and removes an entire class of “we missed one and never noticed”.
Building the receiver
- Verify first, before parsing or acting.
- Return 200 quickly. Do the work asynchronously; there is a 15-second timeout.
- Deduplicate on
event_id. - Handle unknown events. More get added; do not throw on one you do not recognise.
- Reconcile from the event log on a schedule.
Something missing or out of date? Email [email protected] — docs corrections go straight to the team that builds the feature.