Webhooks
A webhook is the same event stream as events, pushed to your server. Webhook routes are for people only (a signed-in user or a personal token with manage access); a service principal cannot call them.
A Workload can have up to 5 webhooks. Each has an https URL on a public host, an event_types filter (a list, or all) and a state: active, failing or disabled. Creating one returns {webhook, secret}: the signing secret, whsec_ and 43 characters, is shown once (and again only by rotate-secret). A new webhook starts with the events after the moment you create it. disabled_reason is failing_72h when it was disabled by repeated failure, otherwise null.
| Route | Does |
|---|---|
GET, POST .../units/webhooks | list, create |
GET, PATCH, DELETE .../units/webhooks/{webhook_id} | read, change, delete |
POST .../units/webhooks/{webhook_id}/rotate-secret | new secret; the old one keeps signing for 24 hours |
POST .../units/webhooks/{webhook_id}/test | sends a webhook.ping |
POST .../units/webhooks/{webhook_id}/enable | re-enable a disabled webhook |
GET .../units/webhooks/{webhook_id}/deliveries | attempts, newest first: time, event range (from_seq, to_seq), status code, latency, error, first 4 KiB of your answer, next attempt (kept 30 days). limit 1 to 100 (default 50), before pages back |
GET .../units/webhooks/metrics | per webhook over the last window_hours (1 to 720, default 24): succeeded, failed, success_rate, average, p95 and max latency |
test answers with the attempt itself (status, latency, error) and is limited to 6 a minute per webhook. It works on a disabled webhook too and does not move the cursor. Changing the url or event_types with PATCH keeps delivering from the same cursor; event_types: [] means every type.
curl
Code
TypeScript
Code
Python
Code
What you receive
A POST of {webhook_id, delivery_id, events: [...]}: at most 50 events or 256 KiB, in seq order, at least once. The next batch is sent only after your 2xx (within 10 seconds), so order is the promise. Deduplicate on seq.
Headers: Zaroz-Signature: v1=<hex>, Zaroz-Timestamp (Unix seconds), Zaroz-Webhook-Id, Zaroz-Delivery-Id.
Verify the signature
The signature is HMAC-SHA256(secret, "{timestamp}.{raw body}") in hex. During a rotation the header holds two v1= values, comma separated: accept either. Refuse a timestamp more than 5 minutes away. Verify the raw bytes you received, not a re-serialised body.
curl
Code
TypeScript
Code
Python
Code
Retries and disabling
Failed deliveries retry after 5 s, 30 s, 2 min, 10 min, 30 min, 1 h, then hourly. A webhook failing for 72 hours becomes disabled (failing_72h) and the organization owners are notified; its cursor stays. After enable, delivery resumes from the cursor if the events are still kept, else from the oldest kept event, preceded by an events.gap {from_seq, to_seq} event so you know you missed some.
What the platform refuses to call
The target is resolved once per attempt and every address is checked: loopback, private, link-local, carrier-grade NAT, multicast, unique-local IPv6 and the platform's own ranges are refused. On create or update the answer is an invalid-field-value refusal with address_not_allowed: ... in details; later, at delivery time, the delivery log shows address_not_allowed as the error. The other error values are dns, connect, timeout, http_status, redirect_not_followed, invalid_url and transport. A sixth webhook is refused with 409 business-rule-violation (too_many_webhooks in details). Redirects are not followed. Only the first 4 KiB of your response is read.
