You’ll need
- An API key with
webhooks:writeandwebhooks:read. - A public HTTPS URL. Private, loopback, and metadata IPs are blocked at both create time and every delivery.
1. Create a subscription
Savesigning_secret from the response — Meow returns it once.
Response
2. Verify the signature
Three headers come with every delivery:| Header | What it is |
|---|---|
webhook-id | Delivery ID. Same value on every retry. |
webhook-timestamp | Epoch seconds. New on each attempt. |
webhook-signature | One or more v1,<base64-hmac> entries, space-separated. Multiple during secret rotation. |
f"{webhook-id}.{webhook-timestamp}.{body}" with HMAC-SHA-256. The
HMAC key is the base64-decoded body of your whsec_<base64> secret —
not the raw string. Reject anything more than 5 minutes off — that’s the
replay window.
3. Dispatch on payload.status
Event names stay coarse — {resource}.created and {resource}.updated.
The lifecycle stage lives on the payload, so one handler covers the whole
flow:
4. Pick a payload mode
Setpayload_mode per subscription. Change it any time with PATCH.
- snapshot (default)
- thin
data carries the full resource. No follow-up GET needed.5. Handle out-of-order deliveries
Deliveries are not ordered. Retries, redrives, and concurrent workers mean an older state for a resource can land after a newer one. Two fields let you stay correct. Every resource event carries asequence — a counter that increments
once per resource, the same for every subscriber, stable across retries and
redrives. Track the highest sequence you’ve applied per resource and
advance it with a single atomic conditional write, so a stale delivery
can’t overwrite newer state:
sequence is absent on webhook.test and message.attempt.exhausted (they
describe no resource). For everything else, pair it with deduplication on
webhook-id (next section): sequence discards stale states, webhook-id
discards exact duplicates.
6. Retries
Meow retries up to 10 times over ~91 hours.| Attempt | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| Delay before | 0 | 5 s | 5 m | 30 m | 2 h | 5 h | 10 h | 14 h | 20 h | 24 h |
- Each non-zero delay gets up to 20% extra jitter.
Retry-Afteron a 429 or 503 is honored, up to 24 hours.- 5 failures in a row open a circuit breaker on the subscription. New deliveries wait out a cooldown (1 → 30 min) without burning an attempt. A success closes it.
- After 10 failed attempts the subscription is auto-disabled
(
disabled_reason=retry_exhausted) and amessage.attempt.exhaustedevent fires.
PATCH /webhooks/subscriptions/{id} with {"is_enabled": true},
or replay a single delivery with POST /webhooks/deliveries/{id}/redrive.
7. Operate
Send a test event
Sends
webhook.test to one subscription. Good for verifying a new
receiver without waiting for real activity.Inspect delivery history
Every attempt with HTTP status and response body excerpt.
Redrive a delivery
Resets the counter and re-queues. Works even after
failed_permanent.Rotate the secret
PATCH with rotate_secret: true. Both old and new secrets sign for
7 days; pick whichever your code recognizes.8. Security checklist
Verify the signature
Verify the signature
Constant-time compare.
== leaks the secret.Reject timestamps older than 5 minutes
Reject timestamps older than 5 minutes
Otherwise a leaked payload can be replayed forever.
Dedupe on `webhook-id`
Dedupe on `webhook-id`
Same event can arrive twice (retries, redrives).
webhook-id doesn’t change.Drop stale states with `sequence`
Drop stale states with `sequence`
Advance the highest
sequence per resource with an atomic conditional
write so a stale delivery can’t overwrite newer state.
See Handle out-of-order deliveries.See also
- Event catalog — payload shape per event.
- Standard Webhooks — the wire format.
POST /webhooks/subscriptions— full subscription contract.