Skip to main content
The Workspaces bundle emits webhooks across the invitation lifecycle, on workspace deletion, and on any roster change — so you can keep your own seat/roster mirror and workspace-tagged data in sync. Events use the existing webhook mechanism: HMAC-SHA256 signed, subscription-gated by the events you enable in the dashboard, fire-and-forget (no retry).

Event catalog

There are exactly 7 events:
There is no workspace.invite.expired event. Expiry is lazy — a stale invite simply won’t accept; there is no background sweep and thus no moment to fire it. Capability-change events are deliberately deferred, and resend emits no event (the status stays pending — no lifecycle transition).

member.* vs invite.* — two intentionally distinct streams

invite.* tracks the invitation lifecycle; member.* tracks roster sync — any membership row appearing or disappearing. They are NOT redundant, and both are kept.
  • On accept, BOTH fire: workspace.invite.accepted (lifecycle) and workspace.member.added (roster). Dedupe on the ids in the payload if you consume both.
  • member.* also covers non-invite roster changes that have no invite event:
    • member.removed fires on exactly five paths: a leave, a remove, a remove-from-subtree sweep, user-deletion cleanup, and when a user becomes owner and their existing member row is dropped (owner/member rows are disjoint).
    • member.added fires on the transfer-demote case (a previous owner demoted into a fresh member row).
So if you dropped member.* in favor of invite.*, you would lose every targeted removal transition (leaves, removals, offboarding) — which is why member.* is the stream to mirror your live seat/roster set.

Deleting a workspace does NOT emit per-member events

DELETE /workspaces/:id emits workspace.deleted once per node in the cascade, and nothing per membership row. There is no workspace.member.removed for the memberships it tears down — not for the deleted workspace, and not for any descendant.Treat workspace.deleted as removing that workspace’s entire roster: on receiving it, drop every seat you hold for that workspace id wholesale. Because the cascade fires one workspace.deleted per node, doing this for each event you receive clears the whole subtree.
This is deliberate. A workspace delete tears down every membership in the subtree at once, so a 500-member tree would fan out 500+ POSTs in a single burst against a delivery layer with no retry and no batching — and every one of them would be redundant with the workspace.deleted that accompanies it. The five paths that do fire member.removed are all bounded, targeted removals where the row is the only thing that changed and no other event would tell you about it. Note the asymmetry with user-deletion cleanup: deleting a user does still fire member.removed per row, because there is no workspace.deleted to stand in for it — the workspaces survive, only that user’s seats disappear.
The idempotent already-member accept is a no-op: it marks the invite accepted (so invite.accepted still fires) but creates no membership row, so no member.added fires.

Uninstalling the bundle

The Workspaces bundle is purely additive — it touches no other bundle’s tables, so uninstall has empty cross-bundle actions. Uninstalling drops the three workspace tables (invitations → members → workspaces) and the data in them.
Uninstall fires NO per-workspace webhook. Unlike deleting workspaces one at a time (which fires workspace.deleted per workspace), tearing the whole bundle out does not emit a workspace.deleted for each workspace. Your own data tagged with workspace ids is your responsibility — Sublay never touches it and does not notify you per-workspace on uninstall. Back up or reconcile before uninstalling.