Receiving Deliveries
What arrives at your endpoint, how to verify it, and what your response means.
The request
Each delivery is a POST with a JSON body and these headers (captured
verbatim, signature trimmed):
Content-Type: application/json
User-Agent: snug-api-webhook/1.0
X-Webhook-Id: vPjqTYTwBqWYMvRWUxYq
X-Webhook-Event: test.webhook
Snug-Signature: t=1787893347,v1=906b244f44ea02bc996c1003c9ef9de99...
X-Webhook-Timestamp: 1787893347
X-Request-Id: evt_eFrjPvMZJZfZ
X-Delivery-Attempt: 1
X-Docs4: demoX-Request-Id is the event ID - the same event redelivered on retry keeps
its ID, so use it for idempotency. X-Delivery-Attempt counts retries of
that event. Custom headers configured on the webhook (X-Docs4 above) are
appended to every delivery.
The payload
{
"id": "evt_eFrjPvMZJZfZ",
"type": "test.webhook",
"message": "docs4 ping",
"timestamp": 1787893347,
"created": 1787893347
}id, type, and created (enqueue time, epoch seconds) are always
present; the remaining fields are the event's own, flattened into the same
object. Per event type:
entity.status_changed-entity_id,previous_status,current_status(alive/dead),last_liveness,metadataentity.liveness_anomaly-entity_id,anomaly_type(high_network_delay/clock_drift/rapid_status_change),network_delay_ms,threshold_ms,metadatarate_limit.exceeded-token_id,endpoint,limit,window_seconds,attemptstest.webhook-message,timestamp
Verifying the signature
Snug-Signature is t=<epoch seconds>,v1=<hex HMAC-SHA256>. The signed
message is the timestamp, a dot, and the raw request body - verify
against the exact bytes received, before any JSON parsing or re-encoding.
This example reproduces the captured signature above from the webhook's
secret:
import hmac, hashlib
def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
t_part, v1_part = header.split(",")
timestamp, signature = t_part[2:], v1_part[3:]
# reject timestamps outside the tolerance window to prevent replay
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
verify("whsec_V8QpK6nDjdTZr9rTRCqz2_Y9lnQtIG0SOalFld6YsHc",
b'{"id":"evt_eFrjPvMZJZfZ","type":"test.webhook","message":"docs4 ping",'
b'"timestamp":1787893347,"created":1787893347}',
"t=1787893347,v1=906b244f44ea02bc996c1003c9ef9de9928ec8cc1f9c4799cc10231c0f0a22c2")
# TrueThe server signs with a fresh timestamp on every attempt and tolerates 5
minutes of clock skew when verifying its own scheme - enforce the same
window and use a constant-time comparison, as above. After
rotate-secret, deliveries are signed with the new secret immediately.
What your response means
2xx- delivered. The webhook's consecutive-failure counter resets.4xxexcept429- permanent failure. No retry: the event dead-letters after that single attempt (a404receiver produces one delivery attempt and a DLQ entry withattempt_count: 1).- Anything else -
5xx,429, timeouts, connection failures - is retried with exponential backoff until the attempt budget (default 5) is exhausted, then dead-lettered.
Respond quickly - the delivery request times out after 30 seconds by
default (WEBHOOK_REQUEST_TIMEOUT_SECONDS). Accept the delivery first and
process it afterwards; a slow handler that times out counts as a failed
attempt and, repeated, will auto-disable the webhook.
Failed attempts are visible from the sending side in
snug webhooks deliveries (status code or error text per event) and the
DLQ commands on the main page.