Skip to content

Webhooks: outbound events with a persisted, retried delivery queue#82

Merged
jwicks31 merged 1 commit into
mainfrom
claude/webhooks
Jun 13, 2026
Merged

Webhooks: outbound events with a persisted, retried delivery queue#82
jwicks31 merged 1 commit into
mainfrom
claude/webhooks

Conversation

@jwicks31

Copy link
Copy Markdown
Owner

What

Outbound webhooks with a persisted, retried delivery queue — the integration primitive, designed to be stable so subscribers don't have to relearn it. Per your steer: durable delivery + data.*/file.* events, HMAC-signed.

API (stable surface)

Management is key-gated and per-project:

  • POST /v1/webhooks { url, events[], collections?, secret?, active? } → returns the hook + signing secret (shown once)
  • GET /v1/webhooks (secrets omitted) · GET/PATCH/DELETE /v1/webhooks/:id · POST /v1/webhooks/:id/rotate-secret
  • GET /v1/webhooks/:id/deliveries — recent attempts with status / last error / status code

Events use a resource.action taxonomy — data.created · data.updated · data.deleted · file.created · file.deleted — subscribed by exact type, prefix (data.*), or *, with an optional collections filter. New event types can be added later without breaking existing subscribers.

Delivery: each is a JSON POST { id, event, createdAt, project, data } with X-Zero-Event / X-Zero-Delivery / X-Zero-Timestamp / X-Zero-Signature: sha256=HMAC(secret, "<timestamp>.<body>").

How

  • Two tables (webhooks, webhook_deliveries) on both SQLite and Postgres. On a write, matching active hooks are enqueued as durable delivery rows (the active-hook list is cached briefly, so the no-webhook common case stays off the DB).
  • A background dispatcher claims due rows atomically (so it's safe to run one per process), POSTs with a timeout, and retries non-2xx/errors with exponential backoff up to WEBHOOK_MAX_ATTEMPTS, recording status. Enqueue is best-effort — a webhook failure never blocks the originating data/file write. The delivery row snapshots url+secret, so editing/deleting a hook can't orphan in-flight deliveries.
  • SDK: client.webhooks.{create,list,get,update,rotateSecret,remove,deliveries}; discovery (/v1) advertises events + the signing scheme. Config: WEBHOOK_MAX_ATTEMPTS, WEBHOOK_TIMEOUT_MS.

Verification

  • SQLite 69/69, Postgres 51/51 (Postgres 16) — including: a data.created event signed + delivered, signature re-verified in the test, delivery recorded as delivered; a failing (500) endpoint retried then marked failed at the cap with lastStatusCode; and URL/event validation 400s. (Webhook tests live in api.test.ts, so they run on both backends — exercising the portable boolean active handling.)
  • Docs: README (capability + a Webhooks section with the header/signing reference), /docs (new Webhooks REST section), .env.example.

Note: webhooks make outbound requests; management is key-gated, and the docs advise running with a key + restricting egress when internet-exposed.

Next: per-project AI provider config, then verifiable identity (JWT/OIDC).

https://claude.ai/code/session_018efxvWw3MRjdtvE5xgBqya


Generated by Claude Code

Register URLs to receive data.* / file.* events as signed HTTP POSTs, with
durable at-least-once delivery — the headline integration primitive.

- Management API (key-gated, per-project): POST/GET /v1/webhooks,
  GET/PATCH/DELETE /v1/webhooks/:id, POST :id/rotate-secret, and
  GET :id/deliveries (recent attempts + status). create/get/rotate return the
  signing secret; list omits it. URL must be http(s); events validated.
- Event taxonomy is resource.action (data.created/updated/deleted,
  file.created/deleted), matched by exact type, prefix (data.*), or * — and an
  optional per-collection filter. Extensible without breaking subscribers.
- Persistence: two tables (webhooks, webhook_deliveries) on both backends. On a
  write, matching active hooks are enqueued as durable delivery rows (active-hook
  list cached briefly so the no-webhook common case stays off the DB). A
  background dispatcher claims due rows atomically (multi-process safe), POSTs
  with a 10s timeout, and retries non-2xx/errors with exponential backoff up to
  WEBHOOK_MAX_ATTEMPTS, recording status/last error. Delivery is best-effort from
  the writer's side: a webhook failure never blocks the originating write.
- Signing: X-Zero-Signature: sha256=HMAC(secret, "<X-Zero-Timestamp>.<body>"),
  plus X-Zero-Event / X-Zero-Delivery headers. Stable { id, event, createdAt,
  project, data } payload envelope.
- SDK: client.webhooks.{create,list,get,update,rotateSecret,remove,deliveries};
  discovery advertises events + signing. Config: WEBHOOK_MAX_ATTEMPTS / _TIMEOUT_MS.

Verification: 69/69 SQLite + 51/51 Postgres 16 — signed delivery + signature
verification + delivery history (delivered), retry-then-failed at the cap, and
URL/event validation. Docs updated (README, /docs, .env.example).
@jwicks31 jwicks31 merged commit a47cd3e into main Jun 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants