Naming convention
Every event internal follows<resource>.<action> (one dot). Nested URL
segments join with hyphens; domain is the first path segment after
/api/v3 and is stored on the event row:
Rules worth knowing:
- Only transactional methods (
POST,PATCH,PUT,DELETE) produce events.GETnever does. - Resources that live directly under the domain (e.g.
/api/v3/account) collapse to<domain>.<action>—account.created. - Platform and infrastructure routes (
admin,auth,cron,integration,oauth,queue,realtime, the webhook module itself) are excluded. Third-party I/O never becomes a public event. webhook.testis the only hand-written entry. It is the synthetic event sent by the Test action; create andPUT …/eventsalways keep it subscribed.- Internals are capped at 120 characters, must match
^[a-z0-9-]+\.[a-z0-9-]+$, and must be unique; the generator fails otherwise. Collisions append-{method}to the action.
Account scope
Each event has anaccount_scope that decides which subscriptions can
receive it:
*.purged events are always none: the row is gone by the time the event
is routed, so its owner cannot be read anymore.Generated vs synced
The catalogue exists twice, on purpose:- Code —
lib/webhook/catalogue/webhook-event-catalogue.data.ts, regenerated withpnpm --filter @bbrandslab/horizon-api webhook:catalogue:generate. CI runs--checkso a new mutating route cannot merge without its event. - Database — the
eventtable, upserted per environment bywebhook:catalogue:syncright after migrations. Subscriptions (webhook_event) referenceevent.document_id.
GET /api/v3/webhook/catalogue/status compares both and reports
missing_internals (in code, not yet synced), stale_internals (in the
database, no longer produced) and malformed_internals (manual rows whose
internal has no .). Horizon Enterprise shows a banner on the Events
screen while they differ. The catalogue HTTP API is read-only (list,
detail, export).