Platform - Webhooks

In this guide, we will explain how to use webhooks within your product so that you can react to Liveblocks events as they happen. Now that our platform is observable, you can use webhooks to reduce development time and the need for polling.

Configuring webhooks

To set up webhooks for your project, you’ll need to create an endpoint, subscribe to events, and secure your endpoint.

Creating an endpoint

If you would like to create an endpoint to receive webhook events, you will do so from within the webhooks dashboard for your project.

  1. From the dashboard overview, navigate to the project you would like to add webhooks to
  2. Click on the webhooks tab from the lefthand menu
  3. Click the “Add Endpoint” button
  4. Enter the URL of the endpoint you would like to use and a description of it
  5. Select the events you would like to subscribe to (if you make no selections, all events will be sent to the endpoint)

Subscribing to events

By default, each endpoint provided in the webhooks dashboard listens to all events. However, you can easily configure it to only listen to a subset of events by updating the "Subscribed events" section after creating the endpoint

  1. Select the endpoint you would like to edit from the list of webhooks in the dashboard
  2. Click the “Edit” button next to the “Subscribed events” section
  3. Update event selections and click “Save”

Replaying events

If your service is unreachable, message retries are automatically re-attempted. If your service incurs considerable downtime (over 8 hours), you can replay individual messages from the Endpoints portion of the dashboard by clicking the kebab menu on an individual message, or you can opt to bulk replay events by clicking the main menu and selecting “Replay Failed Messages.”

Each message is attempted based on a schedule that follows the failure of the preceding attempt. If an endpoint is removed or disabled, delivery attempts will also be disabled. The schedule for retries is as follows:

  • Immediately
  • 5 seconds
  • 5 minutes
  • 30 minutes
  • 2 hours
  • 5 hours
  • 10 hours
  • 10 hours (in addition to the previous)

For example, an attempt that fails three times before eventually succeeding will be delivered roughly 35 minutes and 5 seconds following the first attempt.

Security verification

Verifying webhooks prevents security vulnerabilities by safeguarding against man-in-the-middle, CSRF, and replay attacks. Because of this, it is essential to prioritize verification in your integration.

There are two ways to verify your webhooks, either manually or by using the @liveblocks/node package. We highly recommend using the @liveblocks/node package to verify and return fully typed events.

With @liveblocks/node

1. Install the package

$npm install @liveblocks/node

2. Setup the webhook handler

import { WebhookHandler } from "@liveblocks/node";
const webhookHandler = new WebhookHandler(process.env.SECRET); // "whsec_..." obtained from the webhook dashboard

3. Verify an event request

const event = webhookHandler.verifyRequest({  headers: req.headers,  rawBody: req.body,});

The method will return a WebhookEvent object that is fully typed. You can then use the event to perform actions based on the event type.

If the request is not valid, an error will be thrown.

Example

import { WebhookHandler } from "@liveblocks/node";
// Will fail if not properly initialized with a secretconst webhookHandler = new WebhookHandler(process.env.WEBHOOK_SECRET);
export default function webhookRequestHandler(req, res) { try { const event = webhookHandler.verifyRequest({ headers: req.headers, rawBody: req.body, });
// do things with the event } catch (error) { console.error(error); return res.status(400).end(); }
res.status(200).end();}

Manual Process

1. Construct the signed content

The content to sign is composed by concatenating the request’s id, timestamp, and payload, separated by the full-stop character (.). In code, it will look something like:

const crypto = require("crypto");
// webhookId comes from the `webhook-id` header// webhookTimestamp comes from the `webhook-timestamp` header// body is the request bodysignedContent = `${webhookId}.${webhookTimestamp}.${body}`;

2. Generate the signature

Liveblocks uses an HMAC with SHA-256 to sign its webhooks.

So to calculate the expected signature, you should HMAC the signedContent from above using the base64 portion of your signing secret (this is the part after the whsec_ prefix) as the key. For example, given the secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw you will want to use MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw.

For example, this is how you can calculate the signature in Node.js:

// Your endpoint’s signing secretconst secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// Need to base64 decode the secretconst secretBytes = new Buffer(secret.split("_")[1], "base64");// This is the signature you will compare against the signature headerconst signature = crypto .createHmac("sha256", secretBytes) .update(signedContent) .digest("base64");

3. Validate the signature

The generated signature should match one of the signatures sent in the webhook-signature header.

The webhook-signature header comprises a list of space-delimited signatures and their corresponding version identifiers. The signature list is most commonly of length one. Though there could be any number of signatures. For example:

v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo=

Make sure to remove the version prefix and delimiter (e.g., v1) before verifying the signature.

4. Verify the timestamp

As mentioned above, Liveblocks also sends the timestamp of the attempt in the webhook-timestamp header. You should compare this timestamp against your system timestamp and make sure it’s within your tolerance to prevent timestamp attacks.

Liveblocks Events

An event occurs when a change is made to Liveblocks data. Each endpoint you provide in the webhooks dashboard listens to all events by default but can be easily configured to only listen to a subset by updating the Message Filtering section.

The Event Catalog in the webhooks dashboard provides a list of events available for subscription, along with their schema.

Events available for use include:

  • StorageUpdated
  • UserEntered/UserLeft
  • RoomCreated/RoomDeleted

More events will come later, such as:

  • MaxConnectionsReached

UserEnteredEvent

When a user connects to a room, an event is triggered, indicating that the user has entered. The numActiveUsers field shows the number of users in the room after the user has joined. This event is not throttled.

// Schematype UserEnteredEvent = {  type: "userEntered";  data: {    appId: string;    roomId: string;    connectionId: number;    userId: string | null;    userInfo: Record<string, any> | null;    enteredAt: string;    numActiveUsers: number;  };};
// Exampleconst userEnteredEvent = { type: "userEntered", data: { appId: "my-app-id", roomId: "my-room-id", connectionId: 4, userId: "a-user-id", userInfo: null, enteredAt: "2021-10-06T01:45:56.558Z", numActiveUsers: 8, },};

UserLeftEvent

A user leaves a room when they disconnect from a room, which is when this event is triggered. The numActiveUsers field represents the number of users in the room after the user has left. This event, like UserEntered, is not throttled.

// Schematype UserLeftEvent = {  type: "userLeft";  data: {    appId: string;    roomId: string;    connectionId: number;    userId: string | null;    userInfo: Record<string, any> | null;    leftAt: string;    numActiveUsers: number;  };};
// Exampleconst userLeftEvent = { type: "userLeft", data: { appId: "my-app-id", roomId: "my-room-id", connectionId: 4, userId: "a-user-id", userInfo: { name: "John Doe", }, leftAt: "2021-10-06T01:45:56.558Z", numActiveUsers: 7, },};

StorageUpdatedEvent

Storage is updated when a user writes to storage. This event is throttled at five minutes and, as such, may not be triggered for every write.

For example, if a user writes to storage at 1:00 pm, the StorageUpdatedEvent event will be triggered shortly after. If the user writes to storage again at 1:01 pm, the StorageUpdatedEvent event will be triggered 5 minutes after the first event was sent, around 1:05 pm.

// Schematype StorageUpdatedEvent = {  type: "storageUpdated";  data: {    roomId: string;    appId: string;    updatedAt: string;  };};
// Exampleconst storageUpdatedEvent = { type: "storageUpdated", data: { appId: "my-app-id", roomId: "my-room-id", updatedAt: "2021-10-06T01:45:56.558Z", // 👈 time of the last write },};

RoomCreatedEvent

An event is triggered when a room is created. This event is not throttled. There are two ways for rooms to be created:

  • By calling the create room API
  • When a user connects to a room that does not exist
// Schematype RoomCreatedEvent = {  type: "roomCreated";  data: {    appId: string;    roomId: string;    createdAt: string;  };};
// Exampleconst roomCreatedEvent = { type: "roomCreated", data: { appId: "my-app-id", roomId: "my-room-id", createdAt: "2021-10-06T01:45:56.558Z", },};

RoomDeletedEvent

An event is triggered when a room is deleted. This event is not throttled.

// Schematype RoomDeletedEvent = {  type: "roomDeleted";  data: {    appId: string;    roomId: string;    deletedAt: string;  };};
// Exampleconst roomDeletedEvent = { type: "roomDeleted", data: { appId: "my-app-id", roomId: "my-room-id", deletedAt: "2021-10-06T01:45:56.558Z", },};

Use Cases

With webhooks, you can subscribe to the events you are interested in, and be alerted of the change when it happens. Powerful ways to leverage webhooks with Liveblocks include:

  • Storage synchronization between room(s) and an internal database
  • Monitoring user activity in a room
  • Notifying the client if maximum concurrency has been reached

Webhooks are an excellent way to reduce development time and the need for polling. By following the steps outlined in this guide, you’ll be able to configure, subscribe to, secure, and replay webhook events with Liveblocks.

If you have any questions or need help using webhooks, please let us know by email or by joining our Discord community! We’re here to help!