Skip to main content

Events and webhooks

An event is a thing that happened to a record. Events are what wires fire on, what notifications are raised from, and what a webhook can carry out to another system.

The event vocabulary

Events are named module.thing.verb. The module owns the name, so the source is always legible:

  • inventory.part.created, inventory.stock.changed, inventory.stock.low
  • purchases.order.arrived, purchases.order_item.received
  • digifab.print.completed, digifab.print.failure_suspected

Each module declares the events it announces in its manifest, and only declared events show up as wire triggers. There is no single generated catalog page yet, so the source of truth is the module itself: read a module's declared events, or list them from the workspace, rather than working from a copy that can drift.

Every event carries a payload that includes the workspace id and, by convention, the id of the record it concerns (partId, taskId, and so on). When an event is emitted it fans out two ways at once: to any module code subscribed to it, and to the user-configured wires that match it.

Outbound: send events to a URL

The webhook notification channel POSTs events to a URL you own. You set it up per workspace as a notification channel (alongside email, Slack, and Discord); see Notifications. The config is a URL and an optional set of headers, so you can carry your own auth token to the receiver.

The body is a stable JSON envelope:

{
"notification_id": "…",
"event_type": "purchases.order.arrived",
"message": "Order from Acme arrived",
"link_url": "/purchases/orders/…",
"priority": "normal",
"org_id": "…",
"user_id": "…",
"occurred_at": "2026-07-11T18:20:00.000Z"
}

A webhook URL that resolves to an internal or loopback address is refused, so a misconfigured hook can not be turned into a probe of the host's own network. On a self-hosted install you can allow a LAN target with a config flag; see Self-hosting.

Slack and Discord are their own channels rather than something you build on top of the raw webhook: point the channel at an incoming webhook URL from that service and Cobblr formats the message for it.

Inbound: let another system ping a workspace

A connector can expose a per-workspace inbound URL that carries a token instead of a login:

POST /api/v1/integrations/<connector>/<token>/webhook

The token resolves to a workspace, so the URL needs no slug and no session. The connector's handler verifies the request (an HMAC signature against a shared secret it stores, for instance), then translates the payload into a Cobblr event that wires and notifications react to. Each hit is audited and its counters bumped, and a token can be revoked, which starts returning 410 Gone. Set these up from Integrations; the available connectors are in the integration catalog.

There is also an instance-level receiver at /api/v1/hooks/<id> for provider webhooks that concern the whole install rather than one workspace (a billing provider's callback, say). Open Cobblr registers no handlers there, so every id returns 404 until an overlay or module adds one.

Wiring inbound to outbound

Because an inbound webhook becomes an ordinary event, a wire can react to it, and that wire's action can raise a notification that an outbound webhook carries onward. The three pieces are separate on purpose, and they compose end to end without any of them knowing about the others.