Skip to content
catchook

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>:

  1. The endpoint is looked up. Config lives behind a 60-second cache, so this is normally not a database read at all.
  2. The signature is verified using the endpoint’s verification_method. A request that fails gets 401 and goes no further.
  3. An event id is generated, and the endpoint’s active destinations are listed.
  4. One delivery job per destination is queued.
  5. The event is recorded — headers, body, and the untouched original payload.
  6. 200 goes 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:

  1. The destination is re-read. If it was deleted or paused since the event arrived, the job is dropped.
  2. The destination’s rate limit is checked. Over budget, the job is retried later rather than dropped.
  3. The raw payload is POSTed to receiver_url, signed with that destination’s own secret.
  4. The attempt is recorded — status, latency, and what your server answered.
  5. On failure, it retries with the destination’s backoff until retry_max_attempts, then stops and the delivery is marked dead.

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-idStable id for the event. Use it as your idempotency key
catchook-endpoint-idWhich endpoint received it
catchook-providerThe endpoint’s provider
catchook-received-atWhen Catchook received it, not when it was delivered
catchook-signatureHMAC 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.