Webhooks
Outbound HTTP event notifications: register an endpoint URL, subscribe it to events from a fixed catalog, and Snug POSTs a signed JSON payload whenever a matching event fires. Delivery is asynchronous through stream workers with retries, a dead-letter queue for exhausted events, and automatic disabling of endpoints that keep failing. Every example on this page was executed against a live server.
When to reach for it: pushing Snug events to external systems - alerting when a liveness entity dies, feeding rate-limit breaches to an ops channel, verifying an integration endpoint with test deliveries.
When not to: webhooks are outbound only - Snug calling you, never you posting events in. Fan-out to connected clients belongs to PubSub; pull-based consumption with ack semantics belongs to Message Queue; running work later on a schedule belongs to the Job Queue.
Concepts
- A webhook is a URL plus a set of subscribed events, owned by its
creator. Platform admins can list and manage every user's webhooks;
everyone else gets
403 forbiddenon webhooks they do not own. - Events come from a fixed catalog -
entity.status_changed,entity.liveness_anomaly,rate_limit.exceeded, andtest.webhook(list them withsnug webhooks events). Subscribing to any other name fails with400 BAD_REQUEST. - Every delivery is signed with the webhook's
whsec_-prefixed secret (HMAC-SHA256 in theSnug-Signatureheader). The secret is returned on create and included ininfo/listresponses - treat that output as sensitive. - Delivery is asynchronous - emitting an event enqueues it; workers deliver, retry with exponential backoff, and dead-letter events that exhaust their attempt budget (5 by default).
- Failing endpoints are auto-disabled after consecutive exhausted events
(10 by default).
enableclears the disabled state and counters.
Manage webhooks
snug webhooks create -u https://example.com/hook \
-e entity.status_changed,rate_limit.exceeded -H '{"X-Custom":"value"}'
snug webhooks list
snug webhooks info <webhook_id>
snug webhooks update <webhook_id> --active false
snug webhooks delete <webhook_id>The webhook ID is positional on every subcommand. -H custom headers (sent
with each delivery) must be a JSON object string. Over HTTP, creation returns
201 with the resource - including the signing secret - in data:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hook", "events": ["test.webhook"]}' \
http://localhost:4000/api/v1/webhooks{
"status": 201,
"msg": "Created",
"data": {
"id": "CTYMFbVtmfnQsUJmeUZh",
"url": "https://example.com/hook",
"events": ["test.webhook"],
"secret": "whsec_UZve9TZRBJPuEuOBkJLbk8tENt746-...",
"active": true,
"headers": null,
"created_at": 1787893342
}
}Timestamps are Unix epoch seconds throughout. The CLI list takes no
filters, but the HTTP GET /api/v1/webhooks speaks the
shared search grammar - verified live with
?q=hook2, ?filter=active:eq:false, and ?sort_by=created_at&sort_order=desc.
Outbound URL guard: the URL must be syntactically valid at create time, and in production builds the delivery client refuses loopback, private, link-local, and cloud-metadata targets (including via DNS rebinding or redirects). Point webhooks at endpoints that are publicly reachable from the API server. (Debug builds relax this, which is how the local captures on these pages were made.)
Test deliveries
test emits a synthetic test.webhook event. It is asynchronous: the
response says the event was enqueued, not that it landed - check deliveries
or your receiver for the outcome:
snug --output json webhooks test <webhook_id> --message 'ping'{ "webhook_id": "vPjqTYTwBqWYMvRWUxYq", "enqueued": true }Two verified gotchas: the target webhook must be active, otherwise
400 invalid_configuration; and the emitted event fans out to every
active webhook subscribed to test.webhook, not just the one you named -
with two subscribed webhooks registered, one test call produced a signed
delivery to both.
What your endpoint receives - headers, payload, signature verification, and the response contract - has its own page: Receiving deliveries.
Delivery history, retries, and the DLQ
snug webhooks deliveries <webhook_id> --limit 20
snug webhooks dlq <webhook_id>
snug webhooks redrive <webhook_id> <event_id>
snug webhooks purge-dlq <webhook_id>A 2xx from your endpoint counts as delivered. A 4xx (except 429) is
treated as permanent and dead-letters immediately; anything else - 5xx,
429, timeouts, connection failures - is retried with exponential backoff
until the attempt budget is exhausted, then dead-lettered. Both paths were
reproduced live; an exhausted connection failure looks like:
{
"message_id": "1787893416051-0",
"webhook_id": "SewcMSNafxyzEPMFAUqF",
"event_id": "evt_HuyXHLtHfdMb",
"event_type": "test.webhook",
"error": "error sending request for url (http://127.0.0.1:4981/docs4-dead)",
"attempt_count": 5,
"enqueued_at": 1787893416,
"dlq_at": 1787893416
}redrive re-enqueues one dead-lettered event (the replayed delivery carries
a fresh redrive_-prefixed ID and the original payload) and removes it from
the DLQ; an unknown event ID is 404 dlq_entry_not_found. purge-dlq
discards everything and reports {"purged": n}.
Auto-disable and recovery
Each event that exhausts its attempts bumps the webhook's
consecutive_failures; a successful delivery resets it. At the limit
(10 by default) the webhook is switched off - reproduced live by driving ten
failed events through one webhook, after which info showed active: false
with disabled_at and disabled_reason: "HTTP 404 Not Found". Deliveries
stop until you fix the receiver and run:
snug webhooks enable <webhook_id> # active again, counters and disabled state cleared
snug webhooks rotate-secret <webhook_id>rotate-secret returns the new secret and signs all subsequent deliveries
with it immediately - update the receiver's verification key first, or
deliveries in the gap will fail verification.
Limits and configuration
Request timeout (30 s), worker counts, retry budget and backoff curve,
auto-disable threshold, and stream/dedupe tuning are all set via the
WEBHOOK_* variables in the
Webhook CONFIG reference.
Reference
- Webhooks API - every endpoint, callable
- Receiving deliveries - the receiver-side contract
- Related: PubSub for client fan-out, Message Queue for pull-based consumption, Job Queue for scheduled work