@specfork/webhooks
Signed, retried, SSRF-safe outbound webhooks for events the requiring spec defines.
Webhooks
Outbound HTTP callbacks. The requiring spec names the events and scopes; this spec delivers them. Signing follows the Standard Webhooks scheme. Not affiliated with or endorsed by Cal.com, Inc. The name is used only to credit the source.
| Area | Prefix | Covers |
|---|---|---|
| webhooks.md | HOOK | Subscriptions, URL safety, signing, retries, logs, tests |
Webhooks
Status: Draft · Prefix: HOOK
Purpose
Send signed HTTP callbacks for events an app defines, reliably and safely.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| HOOK-1 | Subscription: {id, url, events [names defined by the requiring spec], active (default true), secret (generated, ≥ 256 bits, shown once), scope (owner set by the requiring spec), template?, description? (≤ 200 chars), filters? (fields the requiring spec defines)}. (scope, url) unique. |
duplicate url in scope → refused |
| HOOK-2 | URL check on save and before each send: https only; host must not resolve to loopback, private, link-local or metadata addresses. |
https://169.254.169.254 → refused |
| HOOK-3 | Delivery: POST, JSON body {id (delivery id), event, created_at, data}, headers Webhook-Id, Webhook-Timestamp (unix s), Webhook-Signature: v1,<base64 HMAC-SHA256(secret, id + "." + timestamp + "." + body)>. |
receiver verifies signature with secret |
| HOOK-4 | Redirects not followed; timeout 10 s; any non-2xx or timeout is a failure. | receiver 500 → retried |
| HOOK-5 | Retries with backoff: 30 s, 2 min, 10 min, 1 h, 6 h (6 attempts total); same delivery id on every attempt. |
500 then 200 → delivered once, same id |
| HOOK-6 | Each attempt logged {delivery id, attempt, status code, duration, error?}, kept 30 days, visible to the subscription owner; failed deliveries can be resent. |
manual: log shows attempts |
| HOOK-7 | A subscription failing every delivery for 5 consecutive days is set inactive and its owner notified. | manual |
| HOOK-8 | Optional template renders the body from data with a logic-less template language (Mustache); result must be valid JSON, else save is refused. |
invalid template → save refused |
| HOOK-9 | "Send test" posts a sample of a chosen event, returns the receiver's status, and is subject to HOOK-2. | test → status shown |
| HOOK-10 | Events are sent at least once and in no guaranteed order; receivers dedupe by id. |
manual: documented for receivers |
| HOOK-11 | Time-based events (e.g. "starts in") are stored as scheduled deliveries and cancelled when the requiring spec says so. | manual: see requiring spec |
| HOOK-12 | Event patterns: prefix.* matches every event starting with prefix., * matches all; events added later are included. |
subscribe booking.*, new booking.x → delivered |
| HOOK-13 | filters limit deliveries to events whose data match every given field (e.g. inbox_ids); unknown filter fields → save refused. |
filter inbox 1, event for inbox 2 → not sent |
| HOOK-14 | Sends per subscription are limited to 10 at once and 100 per second; excess is queued, never dropped. | 500 events in 1 s → all delivered within ~5 s |
| HOOK-15 | An event whose data lacks a field a subscription filters on is not sent to that subscription. | filter inbox_ids, contact event → not sent |
Out of scope
- Zapier/Make-specific payloads, per-event custom headers.
Decisions
- Signature scheme follows Standard Webhooks (id + timestamp in the signed content blocks replays); the source signed only the body.
- Non-2xx counts as failure and is retried; the source retried only network errors.
From Specfork/webhooks at 2de50d9, licence CC-BY-4.0.