Concepts
How it works
The receive path, the delivery path, and what each one guarantees.
Catchook is two independent services. They share a database and a queue, and nothing else — the receive path never calls the control plane at request time, so webhook ingestion keeps working while the dashboard and API are down.
The receive path
When a provider posts to https://events.catchook.com/e/<endpoint_id>:
- The endpoint is looked up. Config lives behind a 60-second cache, so this is normally not a database read at all.
- The signature is verified using the endpoint’s
verification_method. A request that fails gets401and goes no further. - An event id is generated, and the endpoint’s active destinations are listed.
- One delivery job per destination is queued.
- The event is recorded — headers, body, and the untouched original payload.
200goes back to the provider.
Recording happens after the response is committed, so a slow or unavailable event store never turns into a failed webhook. The trade-off is that recording is best-effort: under an outage of that store you can lose the record of an event that was nonetheless delivered correctly.
Because config is cached for up to 60 seconds, pausing an endpoint, rotating a secret or adding a destination can take that long to take effect on the receive path.
The delivery path
Each queued job is handled independently:
- The destination is re-read. If it was deleted or paused since the event arrived, the job is dropped.
- The destination’s rate limit is checked. Over budget, the job is retried later rather than dropped.
- The raw payload is POSTed to
receiver_url, signed with that destination’s own secret. - The attempt is recorded — status, latency, and what your server answered.
- On failure, it retries with the destination’s backoff until
retry_max_attempts, then stops and the delivery is markeddead.
Two consequences worth designing for:
Delivery is at-least-once. A destination that returns 500 after
successfully processing a request will be sent the same event again. Handlers
should key off catchook-event-id and be idempotent.
Order is not preserved. Jobs are independent and retried independently, so a retried event can land after one that arrived later. If ordering matters, order by a field in the payload, not by arrival.
What your destination receives
The body is the provider’s original bytes, unmodified — so signature
verification against the provider’s secret still works if you want it.
The original headers are forwarded too, minus the ones that described the
connection Catchook terminated rather than the webhook (host,
content-length, content-encoding, cf-*, x-forwarded-*).
Catchook adds:
| Header | |
|---|---|
catchook-event-id | Stable id for the event. Use it as your idempotency key |
catchook-endpoint-id | Which endpoint received it |
catchook-provider | The endpoint’s provider |
catchook-received-at | When Catchook received it, not when it was delivered |
catchook-signature | HMAC over the body, keyed with the destination’s secret |
Retention
Events and delivery attempts are queryable for 14 days. Past that they
are removed, which also means replay cannot reach them.