Webhooks
Status: Current (Scale plan). Warmr can POST events to a URL you configure. Webhook management is an opt-in, secret-bearing surface: reads needwebhooks:read, everything else needs webhooks:write — neither is on a default key.
Event types
Endpoints
Create a webhook — POST /v1/webhooks
Body: { "url", "events"? }. url must be a public https:// URL — a private, loopback, or link-local host is rejected 422 by an SSRF guard. Returns 201:
The
secret is shown exactly once — here. Store it immediately; no later call (GET, PATCH) can ever return it. It is the HMAC key for X-Warmr-Signature. Deleting the webhook destroys the secret with the row.- Omitting
eventssubscribes to ALL events. An explicit"events": []is rejected422(the empty set means “all”, so an empty list is ambiguous — omit the field, or list the events you want). - The create response carries no
created_at/updated_at;GET /v1/webhooksfor timestamps.
Update — PATCH /v1/webhooks/{id}
Partial update of any of url, events, active (at least one, else 422). A new url is re-checked by the SSRF guard. "events": [] is rejected 422 — it would silently widen the subscription to all events; to quiet a webhook, set "active": false instead. Returns { "webhook": Webhook } (never the secret), or 404 if the webhook is not yours.
Delete — DELETE /v1/webhooks/{id}
A hard delete — the delivery history cascades and the secret dies with the row (no undo). Returns { "webhook_id", "deleted": true }.
Test — POST /v1/webhooks/{id}/test
Enqueues a signed webhook.ping delivery and returns 202 with { "delivery_id" }. 202 means enqueued, not delivered — the pump delivers asynchronously; follow the attempt at GET /v1/webhooks/{id}/deliveries.
Redrive — POST /v1/webhooks/deliveries/{id}/redrive
Re-queues a dead delivery for another attempt.
Delivery headers and payload
Every delivery carries three headers:X-Warmr-Event— the event type.X-Warmr-Event-Id— a stable id; use it to dedupe (delivery is at-least-once).X-Warmr-Signature—sha256=<hmac-sha256(rawBody, secret)>.
data carries the event-specific payload.
Delivery guarantees
- At-least-once. The same event may arrive more than once — dedupe on
X-Warmr-Event-Id. - Retries. A failed delivery retries with exponential backoff + jitter, up to 5 attempts over 72 hours.
- Delivery status. A delivery is
pendingwhile it still has attempts left within the window, then becomesdeliveredon success ordeadonce it exhausts the attempts or the 72-hour window. - HTTPS + public only. The target URL must be a public
https://host; the SSRF guard blocks private, loopback, and link-local targets.
Verify the signature
Recompute the HMAC over the raw request body — not the re-serialized JSON, since whitespace and key order must match — and compare in constant time. The SDK’ssdk/src/webhooks.ts exposes two helpers:
verifyWebhookSignature never throws on a bad or missing header — it returns false. constructWebhookEvent verifies and then parses, throwing on an invalid signature.
Without the SDK (Node):