Webhooks: verify, retry, recover
Receive coverage and claim events as they happen — with a signature check that is correct rather than merely present.
Webhooks are the same events the REST API exposes for pull, pushed to you instead. If your system needs to react to a claim being filed, this is the mechanism; polling GET /claims on a timer is the thing it replaces.
Registration happens in the dashboard
Endpoints are registered by the merchant, under Settings → Webhooks. There is no webhooks:* scope and no public endpoint-CRUD route — if you are building an integration for someone else, you give them the URL and they register it.
At registration the merchant is shown a per-endpoint signing secret, once. That secret is what everything below depends on.
The envelope
Every delivery is a POST with the same three top-level fields.
{
"event": "registration.created",
"timestamp": "2026-08-08T12:30:00.000Z",
"data": { }
}data is event-specific. Enrichment fields it cannot fill — order_id, item_id, order_date, warranty_rule_id — arrive as null rather than being omitted, so the field set per event type is stable and you can destructure without guarding every key.
Verify the signature
Each delivery carries X-Warranty-Signature: sha256=<hex> — an HMAC-SHA256 of the raw request body, keyed with the endpoint secret.
Compute the HMAC over the raw bytes, before any JSON parsing. If your framework has already parsed and re-serialised the body, the bytes have changed — key order, whitespace, number formatting — and the signature will never match. In Express that meansexpress.raw()on this route, notexpress.json().
const crypto = require("crypto");
function verify(rawBody, header, secret) {
const digest = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const expected = `sha256=${digest}`;
const a = Buffer.from(expected);
const b = Buffer.from(header ?? "");
// timingSafeEqual THROWS on length mismatch, so the length check is not
// an optimisation — without it a malformed header crashes the handler
// instead of being rejected. Comparing lengths first leaks nothing:
// the expected length is fixed and public.
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Use a timing-safe comparison rather than ===. A plain string compare returns early on the first differing byte, which leaks enough timing information to forge a signature given enough attempts.
There is also a User-Agent: Warrantini-Webhooks/1.0 header. It is a convenience for log filtering and not authentication — anyone can send it.
Reply fast, work later
Deliveries time out after 10 seconds. Your handler should verify the signature, persist the event, return 2xx, and do the real work afterwards. Anything slower turns a busy period into a retry storm.
Retries
A failed delivery is retried five times with exponential backoff:
- 1 minute
- 5 minutes
- 30 minutes
- 2 hours
- 8 hours
After the fifth failure the delivery is dead-lettered and never retried. That window is long — over eight hours — but it is finite, so an endpoint down for a working day loses events permanently.
Endpoint health, and the part that surprises people
After 5 consecutive failures across any events, the endpoint is marked unhealthy and the retry cron skips it entirely.
Fixing the endpoint does not resume delivery on its own. Health resets when the signing secret is rotated — so the recovery procedure is: fix the receiver, then rotate the secret in the dashboard. An integration that comes back up and sees nothing arriving is usually sitting in exactly this state.
Handle duplicates
Retries mean the same event can arrive more than once, and a delivery your handler processed but failed to acknowledge in time will be redelivered. Make handlers idempotent — key on the resource id plus event, and make reprocessing a no-op. Do not assume exactly-once.
What you can subscribe to
The events most integrations care about:
registration.created— coverage now exists on a unit.registration.voided— cancelled, typically a refund.registration.expired— the coverage window closed.claim.submitted— a customer filed. Carries the AI triage recommendation and confidence.claim.status_changed— any transition, withprevious_statusandnew_status.claim.resolved— terminal, withresolutionandresolution_type.
There are also assessment, inspection, and ai.* events. The ai.* family only fires for merchants who have the corresponding intelligence feature switched on, so do not build a flow that depends on one arriving — every other event type is unconditional.
Two operational notes
- Endpoint URLs are re-checked at delivery time, not only at registration, and anything resolving to a private, loopback, or link-local address is refused. That is a DNS-rebinding guard — a public hostname that later points inward will not be delivered to.
- Delivery is fire-and-forget. A webhook failure never blocks or fails the API call, admin action, or customer submission that triggered it. Your outage is not the merchant's outage.
Next
Errors, idempotency, pagination, and rate limits — the same robustness thinking applied to the calls you make outbound.