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

EventFires when
review.createdA review is submitted
review.approvedA review is published
review.rejectedA review is rejected
review.updatedA review is edited
review.video_readyAn attached video finishes transcoding and is playable
order.placedAn order is first seen
order.completedAn order reaches a completed or shipped status
gallery.submittedA customer submits a photo to a gallery
gallery.approvedA merchant publishes a submitted item
redemption.createdA customer spends points
redemption.usedA redemption is used
redemption.expiredA 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_ready exists because review.created can arrive while the video is still transcoding. If you re-render a review with playback, listen for this rather than retrying on review.created.
  • redemption.created carries the discount code and a fulfillment_status. On a headless store, or for any reward the platform cannot express, that arrives as unsupported — 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

HeaderContents
x-evident-signatureHMAC-SHA256 of the raw body, hex, keyed with your webhook secret
x-evident-eventThe event name, so you can route before parsing
x-evident-webhook-idUnique 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

  1. Verify first, before parsing or acting.
  2. Return 200 quickly. Do the work asynchronously; there is a 15-second timeout.
  3. Deduplicate on event_id.
  4. Handle unknown events. More get added; do not throw on one you do not recognise.
  5. 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.