> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sublay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks & lifecycle

> The workspace webhook event catalog, the member.* vs invite.* streams, and uninstall behavior

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](/sdk/app-notifications/webhook-integration): 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:

| Event                       | Fires when                                                                                          |
| --------------------------- | --------------------------------------------------------------------------------------------------- |
| `workspace.invite.created`  | An invitation is created.                                                                           |
| `workspace.invite.accepted` | An invitation is accepted.                                                                          |
| `workspace.invite.declined` | An invitation is declined.                                                                          |
| `workspace.invite.revoked`  | An invitation is revoked.                                                                           |
| `workspace.deleted`         | A workspace is deleted (one per workspace actually deleted, including each node in a cascade).      |
| `workspace.member.added`    | A membership **row** is created.                                                                    |
| `workspace.member.removed`  | A membership **row** is removed by a *targeted* action — **not** by a workspace delete (see below). |

<Note>
  **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).
</Note>

## `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

<Warning>
  **`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.
</Warning>

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.

<Note>
  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.
</Note>

## 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.

<Warning>
  **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.
</Warning>

## Related

* [Webhook integration](/sdk/app-notifications/webhook-integration)
* [Delete Workspace API](/api-reference/workspaces/delete-workspace)
* [Workspace Invitation data model](/data-models/workspace-invitation)
