# Liveblocks Webhooks

[Liveblocks](https://liveblocks.io) is realtime collaboration infrastructure: rooms, Storage, Yjs, Comments and notifications. It POSTs a signed JSON event to each endpoint you add in the project's **Webhooks** dashboard, for all events or a filtered subset. Signatures follow Standard Webhooks, so each delivery carries `webhook-id`, `webhook-timestamp` and `webhook-signature` headers.

## When to Use This Skill

* How do I receive Liveblocks webhooks?
* How do I verify a Liveblocks webhook signature (`webhook-signature`)?
* Where do I find my Liveblocks webhook secret (`whsec_...`)?
* How do I handle `userEntered`, `storageUpdated`, `ydocUpdated`, `commentCreated` or `threadCreated` events?
* How do I send emails from Liveblocks `notification` events?
* Why is my Liveblocks webhook verification failing, or failing only on retries and replays?

## Verification (core)

| Header | Value |
| --- | --- |
| `webhook-id` | Identifies the message; retries reuse it. Your idempotency key (the body has no event id) |
| `webhook-timestamp` | Unix seconds |
| `webhook-signature` | One or more space-separated `v1,<base64>` entries |

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`, HMAC-SHA256, base64 output. The key is the base64-**decoded** part of the `whsec_...` secret after the prefix. Compare against every `v1,` entry; ignore other versions. If `webhook-id` is absent, read `svix-id`, `svix-timestamp` and `svix-signature` instead, as Hookdeck does. The examples use the `standardwebhooks` library (npm and PyPI):

```javascript
const { Webhook } = require('standardwebhooks');

// rawBody: exact request body (Buffer or string), NOT re-serialised JSON
function verifyLiveblocks(rawBody, headers, secret = process.env.LIVEBLOCKS_WEBHOOK_SECRET) {
  const p = headers['webhook-id'] ? 'webhook' : 'svix'; // fall back to the svix-* names
  new Webhook(secret).verify(rawBody, {
    'webhook-id': headers[`${p}-id`],
    'webhook-timestamp': headers[`${p}-timestamp`],
    'webhook-signature': headers[`${p}-signature`],
  }, { jsonParse: false }); // throws on a bad signature or stale timestamp → respond 401
}

```

The library always enforces a fixed **5-minute timestamp window**. Hookdeck retries and replays resend the original `webhook-timestamp`, so the library rejects them once they are more than 5 minutes old. If you receive through Hookdeck and need late retries to pass, use the manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/liveblocks-webhooks/references/verification.md), which can drop or widen the window. In Express, read the body with `express.raw({ type: '*/*' })` so any content type verifies, and parse JSON only after the check.

Liveblocks' own `@liveblocks/node` package offers `new WebhookHandler(secret).verifyRequest({ headers, rawBody })`, which verifies and returns a typed event. See [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/liveblocks-webhooks/references/verification.md) for when to pick it.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/liveblocks-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/liveblocks-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/liveblocks-webhooks/examples/fastapi/).

## Event Type and Payload

Every body is the same `{ type, data }` envelope. The event name is the camelCase top-level `type`, and the event fields sit under `data`:

```json
{
  "type": "commentCreated",
  "data": {
    "projectId": "my-project-id",
    "roomId": "my-room-id",
    "threadId": "my-thread-id",
    "commentId": "my-comment-id",
    "createdAt": "2021-10-06T01:45:56.558Z",
    "createdBy": "my-user-id"
  }
}

```

The `notification` event is refined by `data.kind`: `thread`, `textMention`, or a custom `$`-prefixed kind you trigger from your server.

## Common Event Types

| Event | Fired when |
| --- | --- |
| `userEntered` / `userLeft` | A user connects to or disconnects from a room (`numActiveUsers` is the count after the change; not throttled) |
| `storageUpdated` | A user writes to Storage (throttled at 60 s, so not sent for every write) |
| `ydocUpdated` | A user changes a Yjs document in a room (throttled at 60 s) |
| `roomCreated` / `roomDeleted` | A room is created (by API or by a user connecting to a missing room) or deleted |
| `commentCreated` / `commentEdited` / `commentDeleted` | A comment changes |
| `commentReactionAdded` / `commentReactionRemoved` | An emoji reaction is added to or removed from a comment |
| `commentMetadataUpdated` | A comment's metadata is updated |
| `threadCreated` / `threadDeleted` | A thread is created, or deleted (all comments removed or deleted manually) |
| `threadMetadataUpdated` | A thread's metadata is updated |
| `threadMarkedAsResolved` / `threadMarkedAsUnresolved` | A thread's resolved state changes |
| `notification` | A user was mentioned or replied to and hasn't seen it; sent 30 minutes after the activity by default (configurable in project settings), for email notifications |

These 18 are the events the examples route. The docs list them with a note that more will follow (`maxConnectionsReached` is mentioned), so ignore types you don't recognise. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/liveblocks-webhooks/references/overview.md).

## Delivery Semantics

Return a `2xx` to acknowledge. Anything else is retried, 8 attempts in all (immediately, then after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h and 10 h). If every delivery to an endpoint fails for 5 consecutive days, Liveblocks disables it; re-enable it from the dashboard. After long downtime (over 8 hours), replay messages one at a time or use **Recover failed messages...**. Deduplicate on `webhook-id`. The docs give no timeout or ordering guarantee, so respond quickly, process asynchronously, and use the timestamps in `data` if order matters. Return `400` for missing headers and `401` for a bad signature. Liveblocks no longer sends from a fixed IP list: verify the signature instead of allowlisting.

## Environment Variables

```bash
LIVEBLOCKS_WEBHOOK_SECRET=whsec_xxxxx   # full value from the endpoint's secret in the webhooks dashboard; handlers fail closed (500) without it

```

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 liveblocks --path /webhooks/liveblocks

```

Add the printed URL as an endpoint in the Webhooks dashboard, then choose **Test endpoint...** and **Send example** to deliver a signed event.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/liveblocks-webhooks/references/overview.md) - Event catalog, envelope and payload fields
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/liveblocks-webhooks/references/setup.md) - Add an endpoint, find the secret, filter and test
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/liveblocks-webhooks/references/verification.md) - Library and manual verification, gotchas, debugging