---
meta:
  title: "@liveblocks/node"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/node package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/node` provides you with Node.js APIs for
[authenticating Liveblocks users](#Liveblocks-client) and for
[implementing webhook handlers](#WebhookHandler). This library is only intended
for use in your Node.js back end.

## Liveblocks client [#Liveblocks-client]

The `Liveblocks` client offers access to our REST API.

```ts showLineNumbers={false}
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});
```

### Authentication [#Authorization]

To authenticate your users with Liveblocks, you have the choice between two
different APIs.

- [`Liveblocks.identifyUser`](#id-tokens) ID token authentication is recommend
  for most applications.
- [`Liveblocks.prepareSession`](#access-tokens) Access token authentication is
  best if you prefer handle permissions on your end, though it has
  [limitations](/docs/api-reference/authentication/access-token#limitations).

#### Liveblocks.identifyUser [#id-tokens]

Creates an ID token that is used to authenticate a user in your application.
This is a wrapper around the
[Get ID Token API](/docs/api-reference/rest-api-endpoints#post-identify-user)
and returns the same response.

```ts
const { body, status } = await liveblocks.identifyUser({
  // Required, the current user's ID
  userId: "marie@example.com",
});
```

<Banner>
  Learn how to [get started with ID
  tokens](/docs/api-reference/authentication#id-token).
</Banner>

A number of options are also available, enabling you to set up permissions and
user metadata.

```ts
const { body, status } = await liveblocks.identifyUser(
  {
    // Required, the current user's ID
    userId: "marie@example.com",

    // Optional, only view resources on this organization
    organizationId: "acme-corp",

    // Optional, used to provision room access on group level
    groupIds: ["marketing", "engineering"],
  },
  {
    // Optional, custom user metadata
    userInfo: {
      name: "Marie",
      color: "#00ff00",
      avatar: "https://example.com/avatar/marie.jpg",
    },
  }
);
```

<Banner title="Don't cache tokens" type="warning">

Never cache your access token authentication endpoint, as your client will not
function correctly. The Liveblocks client will cache results for you, only
making requests to the endpoint if necessary, such as when the token has
expired.

</Banner>

##### Granting ID token permissions

You can pass additional options to `identifyUser`, enabling you to create
complex
[workspace permissions](/docs/api-reference/authentication#id-token-workspace-permissions)
and
[room permissions](/docs/api-reference/authentication#id-token-room-permissions).
For example, this user can only see resources in the `acme-corp` workspace, and
they’re part of a `marketing` rooms group within it.

```ts
const { body, status } = await liveblocks.identifyUser({
  // Required, the current user's ID
  userId: "marie@example.com",

  // Optional, only view resources on this workspace
  // +++
  organizationId: "acme-corp",
  // +++

  // Optional, used to provision room access on group level
  // +++
  groupIds: ["marketing"],
  // +++
});
```

<Banner>
  Learn more about [ID token
  permissions](/docs/api-reference/authentication#id-token-room-permissions).
</Banner>

##### Text editor user data

When using
[text editor integrations](/docs/products/sync/integrations#official-packages),
user data is inserted into their live cursor within the editor, showing their
name and color. This data originates from the `userInfo` property.

```ts
const { body, status } = await liveblocks.identifyUser(
  {
    // Required, the current user's ID
    userId: "marie@example.com",
  },
  {
    // Optional, custom user metadata
    userInfo: {
      // Used in text editor live carets
      // +++
      name: "Marie",
      color: "#00ff00",
      // +++
    },
  }
);
```

##### Custom user metadata

You can pass additional options to `prepareSession`, enabling you to add custom
user metadata to the session. This metadata can be accessed by all users in the
room, and is useful for building features such as live avatar stacks.

```ts
const { body, status } = await liveblocks.identifyUser(
  {
    // Required, the current user's ID
    userId: "marie@example.com",
  },
  {
    // Optional, custom user metadata
    userInfo: {
      // Add custom properties to use on front end, e.g. avatar stacks
      // +++
      avatar: "https://example.com/avatar/marie.jpg",
      // +++
      // ...
    },
  }
);
```

To access it on the front end, use hooks such as
[`useSelf`](/docs/api-reference/liveblocks-react#useSelf) and
[`useOthers`](/docs/api-reference/liveblocks-react#useOthers).

```tsx
const currentUser = useSelf();

// "https://example.com/avatar/marie.jpg"
console.log(currentUser.info.avatar);
```

##### How ID tokens work

The purpose of this API is to help you implement your custom authentication back
end (i.e. the _server_ part of the diagram). You use the
`liveblocks.identifyUser()` API if you’d like to issue
[ID tokens](/docs/api-reference/authentication#id-token-authenticating) from
your back end. An ID token does not grant any permissions in the token directly.
Instead, it only securely identifies your user, and then uses any permissions
set via the [Permissions REST API][] to decide whether to allow the user on a
room-by-room basis.

Use this approach if you’d like Liveblocks to be the source of truth for your
user’s permissions.

<Banner title="What are ID tokens?">
  Issuing identity tokens is like issuing _membership cards_. Anyone with a
  membership card can try to enter a room, but your permissions will be checked
  at the door. The Liveblocks servers perform this authorization, so your
  permissions need to be set up front using the Liveblocks REST API.
</Banner>

<Figure>
  <Image
    src="/assets/id-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

Implement your back end endpoint as follows:

```ts showLineNumbers={false}
const { body, status } = await liveblocks.identifyUser(
  {
    userId: "marie@example.com", // Required, user ID from your DB
    groupIds: ["marketing", "engineering"],
    // Optional, identify the user in a specific organization
    organizationId: "acme-corp",
  },

  // Optional
  {
    userInfo: {
      name: "Marie",
      avatar: "https://example.com/avatar/marie.jpg",
    },
  }
);

return new Response(body, { status });
```

`userId` (required) is a string identifier to uniquely identify your user with
Liveblocks. This value will be used when counting unique MAUs in your Liveblocks
dashboard. You can refer to these user IDs in the [Permissions REST API][] when
assigning group permissions.

`groupIds` (optional) can be used to specify which groups this user belongs to.
These are arbitrary identifiers that make sense to your app, and that you can
refer to in the [Permissions REST API][] when assigning group permissions.

`organizationId` (optional) is the organization for this user, will be set to
`default` if not provided.

`userInfo` (optional) is any custom JSON value, which you can use to attach
static metadata to this user’s session. This will be publicly visible to all
other people in the room. Useful for metadata like the user’s full name, or
their avatar URL.

##### ID tokens example

Here’s a real-world example of ID tokens in a Next.js route handler/endpoint.
You can find examples for other frameworks in our
[authentication section](/docs/api-reference/authentication#select-your-framework).

```ts file="Next.js"
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export default async function auth(req, res) {
  /**
   * Implement your own security here.
   *
   * It's your responsibility to ensure that the caller of this endpoint
   * is a valid user by validating the cookies or authentication headers
   * and that it has access to the requested room.
   */

  // Get the current user from your database
  const user = __getUserFromDB__(req);

  // Create an ID token for the user
  const { body, status } = await liveblocks.identifyUser(
    {
      userId: user.id,
    },
    {
      userInfo: {
        name: user.fullName,
        color: user.favoriteColor,
      },
    }
  );

  return new Response(body, { status });
}
```

#### Liveblocks.prepareSession [#access-tokens]

Creates an access token that is used to authenticate a user in your application.
This is a wrapper around the
[Get Access Token API](/docs/api-reference/rest-api-endpoints#post-authorize-user)
and returns the same response.

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com"
);
```

<Banner>
  Learn how to [get started with access
  tokens](/docs/api-reference/authentication/access-token).
</Banner>

A number of options are also available, enabling you to set up permissions and
user metadata.

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com",
  {
    // Optional, only view resources on this organization
    organizationId: "acme-corp",

    // Optional, used to provision room access on group level
    groupIds: ["marketing"],

    // Optional, custom user metadata
    userInfo: {
      name: "Marie",
      color: "#00ff00",
      avatar: "https://example.com/avatar/marie.jpg",
    },
  }
);
```

<Banner title="Don't cache tokens" type="warning">

Never cache your access token authentication endpoint, as your client will not
function correctly. The Liveblocks client will cache results for you, only
making requests to the endpoint if necessary, such as when the token has
expired.

</Banner>

##### Granting access token permissions

Using `session.allow()`, you can grant full or read-only permissions to the user
to select rooms. Wildcards can be used to enable granting permissions to
multiple rooms at once using
[naming patterns](/docs/api-reference/authentication/access-token#Naming-pattern).

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com"
);

// Giving access to an individual rooms
session.allow("room-id-1", ["*:write"]);

// Giving read-only access to this room
session.allow("room-id-2", ["*:read"]);

// Giving access to multiple rooms with a wildcard
// `design-room-1`, `design-room-2`, etc.
session.allow("design-room:*", ["*:write"]);
```

<Banner>
  Learn more about [access token
  permissions](/docs/api-reference/authentication/access-token#permissions).
</Banner>

Additionally, you can pass additional options to `prepareSession`, enabling you
to create complex permissions using
[organizations](/docs/api-reference/authentication/organizations) and
[accesses](/docs/api-reference/authentication/permissions). For example, this
user can only see resources in the `acme-corp` organization, and they're part of
a `marketing` group within it.

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com",
  {
    // Optional, only view resources on this organization
    // +++
    organizationId: "acme-corp",
    // +++

    // Optional, used to provision room access on group level
    // +++
    groupIds: ["marketing"],
    // +++
  }
);
```

##### Text editor user data

When using
[text editor integrations](/docs/products/sync/integrations#official-packages),
user data is inserted into their live cursor within the editor, showing their
name and color. This data originates from the `userInfo` property of the
session.

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com",
  {
    // Optional, user metadata
    userInfo: {
      // Used in text editor live carets
      // +++
      name: "Marie",
      color: "#00ff00",
      // +++
    },
  }
);
```

##### Custom user metadata

You can pass additional options to `prepareSession`, enabling you to add custom
user metadata to the session. This metadata can be accessed by all users in the
room, and is useful for building features such as live avatar stacks.

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com",
  {
    // Optional, custom user metadata
    userInfo: {
      // Add custom properties to use on front end, e.g. avatar stacks
      // +++
      avatar: "https://example.com/avatar/marie.jpg",
      // +++
      // ...
    },
  }
);
```

To access it on the front end, use hooks such as
[`useSelf`](/docs/api-reference/liveblocks-react#useSelf) and
[`useOthers`](/docs/api-reference/liveblocks-react#useOthers).

```tsx
const currentUser = useSelf();

// "https://example.com/avatar/marie.jpg"
console.log(currentUser.info.avatar);
```

##### How access tokens work

The purpose of this API is to help you implement your custom authentication back
end (i.e. the _server_ part of the diagram). You use the
`liveblocks.prepareSession()` API if you’d like to issue
[access tokens](/docs/api-reference/authentication/access-token) from your back
end.

<Banner title="What are access tokens?">
  Issuing access tokens is like issuing _hotel key cards_ from a hotel’s front
  desk (your back end). Any client with a key card can enter any room that the
  card gives access to. It’s easy to give out those key cards right from your
  back end.
</Banner>

<Figure>
  <Image
    src="/assets/access-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

To implement your back end, follow these steps:

<Steps>
  <Step>
    <StepTitle>Create a session</StepTitle>

    <StepContent>
      ```ts showLineNumbers={false}
      const session = liveblocks.prepareSession(
        "marie@example.com",   // Required, user ID from your DB
        {
          // Optional, custom static metadata for the session
          userInfo: {
            name: "Marie",
            avatar: "https://example.com/avatar/marie.jpg",
          },
          // Optional, authenticate this user on a specific organization
          organizationId: "acme-corp",
        }
      );
      ```

      The `userId` (required) is an identifier to uniquely identifies
      your user with Liveblocks. This value will be used when counting
      unique MAUs in your Liveblocks dashboard.

      The `userInfo` (optional) is any custom JSON value, which can be
      attached to static metadata to this user’s session. This will be
      publicly visible to all other people in the room. Useful for
      metadata like the user’s full name, or their avatar URL.

      The `organizationId` (optional) is the organization for this session, will be set to `default` if not provided.

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Decide which permissions to allow this session</StepTitle>
    <StepContent>
      ```ts showLineNumbers={false}
      session.allow("my-room-1", ["*:write"]);
      session.allow("my-room-2", ["*:write"]);
      session.allow("my-room-3", ["*:write"]);
      session.allow("my-team:*", ["*:read"]);
      ```

      <Banner title="Be diligent" type="warning">
        You’re specifying what’s going to be allowed so be careful what
        permissions you’re giving your users. You’re responsible for this
        part.
      </Banner>
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Authorize the session</StepTitle>
    <StepContent>
      Finally, authorize the session. This step makes the HTTP call to the
      Liveblocks servers. Liveblocks will return a signed **access token** that
      you can return to your client.

      ```ts showLineNumbers={false}
      // Requests the Liveblocks servers to authorize this session
      const { body, status } = await session.authorize();
      return new Response(body, { status });
      ```
    </StepContent>

  </Step>

</Steps>

##### Access tokens example [#access-token-example]

Here’s a real-world example of access tokens in a Next.js route
handler/endpoint. You can find examples for other frameworks in our
[authentication section](/docs/api-reference/authentication/access-token).

```ts file="route.ts"
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function POST(request: Request) {
  /**
   * Implement your own security here.
   *
   * It's your responsibility to ensure that the caller of this endpoint
   * is a valid user by validating the cookies or authentication headers
   * and that it has access to the requested room.
   */

  // Get the current user from your database
  const user = __getUserFromDB__(request);

  // Start an auth session inside your endpoint
  const session = liveblocks.prepareSession(
    user.id,
    { userInfo: user.metadata } // Optional
  );

  // Implement your own security, and give the user access to the room
  const { room } = await request.json();
  if (room && __shouldUserHaveAccess__(user, room)) {
    session.allow(room, ["*:write"]);
  }

  // Retrieve a token from the Liveblocks servers and pass it to the
  // requesting client
  const { body, status } = await session.authorize();
  return new Response(body, { status });
}
```

### Room

#### Liveblocks.getRooms [#get-rooms]

Returns a list of rooms that are in the current project. The project is
determined by the secret key you’re using. Rooms are sorted by creation time,
with the newest room at index `0`. This is a wrapper around the
[Get Rooms API](/docs/api-reference/rest-api-endpoints#get-rooms) and returns
the same response.

```ts
const { data: rooms, nextCursor } = await liveblocks.getRooms();

// A list of rooms
// [{ type: "room", id: "my-room-id", ... }, ...]
console.log(rooms);

// A pagination cursor used for retrieving the next page of results with `startingAfter`
// "L3YyL3Jvb21z..."
console.log(nextCursor);
```

A number of options are also available, enabling you to filter for certain
rooms.

```ts
const { data: rooms, nextCursor } = await liveblocks.getRooms({
  // Optional, the amount of rooms to load, between 1 and 100, defaults to 20
  limit: 20,

  // Optional, filter for rooms that allow entry to group ID(s) in `groupsAccesses`
  groupIds: ["engineering", "design"],

  // Optional, filter for rooms that allow entry to a user's ID in `usersAccesses`
  userId: "my-user-id",

  // Optional, use advanced filtering
  query: {
    // Optional, filter for rooms with an ID that starts with specific string
    roomId: {
      startsWith: "liveblocks:",
    },
    // Optional, filter for rooms with custom metadata in `metadata`
    metadata: {
      roomType: "whiteboard",
    },
  },

  // Optional, authenticate this user on a specific organization
  organizationId: "my-organization-id",

  // Optional, cursor used for pagination, use `nextCursor` from the previous page's response
  startingAfter: "L3YyL3Jvb21z...",
});
```

The `query` option also allows you to pass a
[query language](/docs/guides/how-to-filter-rooms-using-query-language) string
instead of a `query` object.

##### Pagination

You can use `nextCursor` to paginate rooms. In this example, when `getNextPage`
is called, the next set of rooms is added to `pages`.

```ts
import { RoomData } from "@liveblocks/node";

// An array of pages, each containing a list of retrieved rooms
const pages: RoomData[][] = [];

// Holds the pagination cursor for the next set of rooms
let startingAfter;

// Call to get the next page of rooms
async function getNextPage() {
  const { data, nextCursor } = await liveblocks.getRooms({ startingAfter });
  pages.push(data);
  startingAfter = nextCursor;
}
```

If you’d like to iterate over all your rooms, it’s most convenient to use
[`liveblocks.iterRooms`](#iter-rooms) instead. This method automatically
paginates your API requests.

#### Liveblocks.iterRooms [#iter-rooms]

Works similarly to [`liveblocks.getRooms`](#get-rooms), but instead returns an
asynchronous iterator, which helps you iterate over all selected rooms in your
project, without having to manually paginate through the results.

```ts
const roomsIterator = liveblocks.iterRooms();

for await (const room of roomsIterator) {
  // { type: "room", id: "my-room-id", metadata: {...}, ... }
  console.log(room);
}
```

A number of options are also available, enabling you to filter for certain
rooms.

```ts
const roomsIterator = await liveblocks.iterRooms({
  // Optional, filter for rooms that allow entry to group ID(s) in `groupsAccesses`
  groupIds: ["engineering", "design"],

  // Optional, filter for rooms that allow entry to a user's ID in `usersAccesses`
  userId: "my-user-id",

  // Optional, use advanced filtering
  query: {
    // Optional, filter for rooms with an ID that starts with specific string
    roomId: {
      startsWith: "liveblocks:",
    },
    // Optional, filter for rooms with custom metadata in `metadata`
    metadata: {
      roomType: "whiteboard",
    },
  },
});

for await (const room of roomsIterator) {
  // { type: "room", id: "my-room-id", metadata: {...}, ... }
  console.log(room);
}
```

The `query` option also allows you to pass a
[query language](/docs/guides/how-to-filter-rooms-using-query-language) string
instead of a `query` object.

##### Mass deleting rooms

You can use `iterRooms` to efficiently delete multiple rooms at once. This
example shows how to delete rooms in batches of 50 concurrent deletions at a
time:

```ts
const MAX_CONCURRENT = 50;
const queue: Promise<void>[] = [];

for await (const room of liveblocks.iterRooms({
  // Optionally filter for certain rooms
  // ...
})) {
  if (queue.length >= MAX_CONCURRENT) {
    await Promise.race(queue);
  }

  const promise = liveblocks
    .deleteRoom(room.id)
    .finally(() => queue.splice(queue.indexOf(promise), 1));

  queue.push(promise);
}

await Promise.all(queue);
```

This approach is useful when you need to delete a large number of rooms, as it
automatically handles pagination and allows you to control the concurrency of
deletions. You can use any of the filtering options shown above to select which
rooms to delete.

#### Liveblocks.createRoom [#post-rooms]

Programmatically creates a new room from a room ID. The `defaultAccesses` option
is required. Setting `defaultAccesses` to `["*:write"]` creates a public room,
whereas setting it to `[]` will create a private room that needs
[ID token permission to enter](/docs/api-reference/authentication#id-token-room-permissions).
This is a wrapper around the
[Create Room API](/docs/api-reference/rest-api-endpoints#post-rooms) and returns
the same response.

```ts
const room = await liveblocks.createRoom("my-room-id", {
  defaultAccesses: ["*:write"],
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

A number of room creation options are available, allowing you to set permissions
and attach custom metadata.

```ts
const room = await liveblocks.createRoom("my-room-id", {
  // The default room permissions. `[]` for private, `["*:write"]` for public.
  defaultAccesses: [],

  // Optional, the room's group ID permissions
  groupsAccesses: {
    design: ["*:write"],
    engineering: ["*:read"],
  },

  // Optional, the room's user ID permissions
  usersAccesses: {
    "my-user-id": ["*:write"],
  },

  // Optional, custom metadata to attach to the room
  metadata: {
    myRoomType: "whiteboard",
  },

  // Optional, create it on a specific organization
  organizationId: "acme-corp",
});
```

Group and user permissions are only used with
[ID token authorization](/docs/api-reference/liveblocks-node#id-tokens), learn
more about
[managing permission with ID tokens](/docs/api-reference/authentication#id-token-room-permissions).

#### Liveblocks.getRoom [#get-rooms-roomId]

Returns a room. Throws an error if the room isn’t found. This is a wrapper
around the
[Get Room API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId) and
returns the same response.

```ts
const room = await liveblocks.getRoom("my-room-id");

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

#### Liveblocks.getOrCreateRoom [#get-or-create-rooms-roomId]

Get a room by its ID. If the room doesn’t exist, create it instead. The
`defaultAccesses` option is required. Setting `defaultAccesses` to `["*:write"]`
creates a public room, whereas setting it to `[]` will create a private room
that needs
[ID token permission to enter](/docs/api-reference/authentication#id-token-room-permissions).
Returns the same response as the
[Create Room API](/docs/api-reference/rest-api-endpoints#post-rooms).

```ts
const room = await liveblocks.getOrCreateRoom("my-room-id", {
  defaultAccesses: ["*:write"],
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

A number of room creation options are available, allowing you to set permissions
and attach custom metadata.

```ts
const room = await liveblocks.getOrCreateRoom("my-room-id", {
  // The default room permissions. `[]` for private, `["*:write"]` for public.
  defaultAccesses: [],

  // Optional, the room's group ID permissions
  groupsAccesses: {
    design: ["*:write"],
    engineering: ["*:read"],
  },

  // Optional, the room's user ID permissions
  usersAccesses: {
    "my-user-id": ["*:write"],
  },

  // Optional, custom metadata to attach to the room
  metadata: {
    myRoomType: "whiteboard",
  },

  // Optional, create it on a specific organization
  organizationId: "acme-corp",
});
```

Group and user permissions are only used with
[ID token authorization](/docs/api-reference/liveblocks-node#id-tokens), learn
more about
[managing permission with ID tokens](/docs/api-reference/authentication#id-token-room-permissions).

#### Liveblocks.updateRoom [#post-rooms-roomId]

Updates properties on a room. Throws an error if the room isn’t found. This is a
wrapper around the
[Update Room API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId) and
returns the same response.

```ts
const room = await liveblocks.updateRoom("my-room-id", {
  // The metadata or permissions you're updating
  // ...
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

Permissions and metadata properties can be updated on the room. Note that you
need only pass the properties you’re updating. Setting a property to `null` will
delete the property.

```ts
const room = await liveblocks.updateRoom("my-room-id", {
  // Optional, update the default room permissions. `[]` for private, `["*:write"]` for public.
  defaultAccesses: [],

  // Optional, update the room's group ID permissions
  groupsAccesses: {
    design: ["*:write"],
    engineering: ["*:read"],
  },

  // Optional, update the room's user ID permissions
  usersAccesses: {
    "my-user-id": ["*:write"],
  },

  // Optional, custom metadata to update on the room
  metadata: {
    myRoomType: "whiteboard",
  },
});
```

Group and user permissions are only used with
[ID token authorization](/docs/api-reference/liveblocks-node#id-tokens), learn
more about
[managing permission with ID tokens](/docs/api-reference/authentication#id-token-room-permissions).

#### Liveblocks.upsertRoom [#upsert-rooms-roomId]

Update a room’s properties by its ID. If the room doesn’t exist, create it
instead. The `defaultAccesses` option is required. Setting `defaultAccesses` to
`["*:write"]` creates a public room, whereas setting it to `[]` will create a
private room that needs
[ID token permission to enter](/docs/api-reference/authentication#id-token-room-permissions).
Returns the same response as the
[Create Room API](/docs/api-reference/rest-api-endpoints#post-rooms).

```ts
const room = await liveblocks.upsertRoom("my-room-id", {
  // These fields will get updated when the room exists, or will be created
  update: {
    metadata: { color: "red" },
  },
  // These fields will only be set when the room will get created
  create: {
    defaultAccesses: ["*:write"],
  },
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

A number of room update or creation options are available, allowing you to set
permissions and attach custom metadata.

```ts
const room = await liveblocks.upsertRoom("my-room-id", {
  update: {
    // The default room permissions. `[]` for private, `["*:write"]` for public.
    defaultAccesses: [],

    // Optional, the room's group ID permissions
    groupsAccesses: {
      design: ["*:write"],
      engineering: ["*:read"],
    },

    // Optional, the room's user ID permissions
    usersAccesses: {
      "my-user-id": ["*:write"],
    },

    // Optional, custom metadata to attach to the room
    metadata: {
      myRoomType: "whiteboard",
    },
  },
});
```

Group and user permissions are only used with
[ID token authorization](/docs/api-reference/liveblocks-node#id-tokens), learn
more about
[managing permission with ID tokens](/docs/api-reference/authentication#id-token-room-permissions).

#### Liveblocks.deleteRoom [#delete-rooms-roomId]

Deletes a room. If the room doesn’t exist, or has already been deleted, no error
will throw. This is a wrapper around the
[Delete Room API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId)
and returns no response.

```ts
await liveblocks.deleteRoom("my-room-id");
```

##### Mass deleting rooms

If you need to delete multiple rooms at once, you can use
[`liveblocks.iterRooms`](#iter-rooms) to efficiently iterate through rooms and
delete them in batches. This example shows how to delete rooms in batches of 50
concurrent deletions at a time:

```ts
const MAX_CONCURRENT = 50;
const queue: Promise<void>[] = [];

for await (const room of liveblocks.iterRooms({
  // Optionally filter for certain rooms
  // ...
})) {
  if (queue.length >= MAX_CONCURRENT) {
    await Promise.race(queue);
  }

  const promise = liveblocks
    .deleteRoom(room.id)
    .finally(() => queue.splice(queue.indexOf(promise), 1));

  queue.push(promise);
}

await Promise.all(queue);
```

You can use any of the filtering options available in
[`liveblocks.iterRooms`](#iter-rooms) to select which rooms to delete, such as
filtering by metadata, room ID prefix, or user/group access.

#### Liveblocks.prewarmRoom [#get-rooms-roomId-prewarm]

Speeds up connecting to a room for the next 10 seconds. Use this when you know a
user will be connecting to a room with
[`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider) or
[`enterRoom`](/docs/api-reference/liveblocks-client#Client.enterRoom) within 10
seconds, and the room will load quicker. This is a wrapper around the
[Prewarm Room API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-prewarm)
and returns no response.

```ts
await liveblocks.prewarmRoom("my-room-id");
```

##### Warm a room before navigating

Triggering a room directly before a user navigates to a room is an easy to way
use this API. Here’s a Next.js server actions example, showing how to trigger
prewarming with `onPointerDown`.

```ts title="actions.ts"
"use server";

import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function prewarmRoom(roomId: string) {
  // +++
  await liveblocks.prewarmRoom(roomId);
  // +++
}
```

```tsx title="RoomLink.tsx"
"use client";

import { prewarmRoom } from "../actions";
import Link from "next/link";

export function JoinButton({ roomId }: { roomId: string }) {
  return (
    // +++
    <Link href={`/rooms/${roomId}`} onPointerDown={() => prewarmRoom(roomId)}>
      // +++
      {roomId}
    </Link>
  );
}
```

`onPointerDown` is slightly quicker than `onClick` because it triggers before
the user releases their pointer.

#### Liveblocks.updateRoomId [#post-rooms-update-roomId]

Permanently updates a room’s ID. `newRoomId` will replace `currentRoomId`. Note
that this will disconnect connected users from the room, but this can be worked
around. Throws an error if the room isn’t found. This is a wrapper around the
[Update Room API](/docs/api-reference/rest-api-endpoints#post-rooms-update-roomId)
and returns the same response.

```ts
const room = await liveblocks.updateRoomId({
  currentRoomId: "my-room-id",
  newRoomId: "new-room-id",
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

##### Redirect connected users to the new room

When a room’s ID is changed it disconnects all users that are currently
connected. To redirect connected users to the new room you can use
[`useErrorListener`](/docs/api-reference/liveblocks-react#useErrorListener) or
[`room.subscribe("error")`](/docs/api-reference/liveblocks-client#Room.subscribe.error)
in your application to get the new room’s ID, and redirect users to the renamed
room.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

function App() {
  useErrorListener((error) => {
    if (error.context.code === 4006) {
      // Room ID has been changed, get the new ID and redirect
      const newRoomId = error.message;
      __redirect__(`https://example.com/document/${newRoomId}}`);
    }
  });
}
```

#### Liveblocks.getActiveUsers [#get-rooms-roomId-active-users]

Returns a list of users that are currently present in the room. Throws an error
if the room isn’t found. This is a wrapper around the
[Get Active Users API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-active-users)
and returns the same response.

```ts
const activeUsers = await liveblocks.getActiveUsers("my-room-id");

// { data: [{ type: "user", id: "my-user-id", ... }, ...] }
console.log(activeUsers);
```

#### Liveblocks.broadcastEvent [#post-broadcast-event]

Broadcasts a custom event to the room. Throws an error if the room isn’t found.
This is a wrapper around the
[Broadcast Event API](/docs/api-reference/rest-api-endpoints#post-broadcast-event)
and returns no response.

```ts
const customEvent = {
  type: "EMOJI",
  emoji: "🔥",
};

await liveblocks.broadcastEvent("my-room-id", customEvent);
```

You can respond to custom events on the front end with
[`useEventListener`](/docs/api-reference/liveblocks-react#useEventListener) and
[`room.subscribe("event")`](/docs/api-reference/liveblocks-client#Room.subscribe.event).
When receiving an event sent with `Liveblocks.broadcastEvent`, `user` will be
`null` and `connectionId` will be `-1`.

```tsx
import { useEventListener } from "@liveblocks/react/suspense";

// When receiving an event sent from `@liveblocks/node`
useEventListener(({ event, user, connectionId }) => {
  // `null`
  console.log(user);

  // `-1`
  console.log(connectionId);
});
```

#### Liveblocks.setPresence [#post-rooms-roomId-presence]

Sets ephemeral presence for a user in a room without requiring a WebSocket
connection. The presence data automatically expires after the specified TTL
(time-to-live). This is useful for scenarios like showing an AI agent’s presence
in a room. The presence is broadcast to all connected users in the room. This is
a wrapper around the
[Set Presence API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-presence)
and returns no response on success.

```ts
await liveblocks.setPresence("my-room-id", {
  userId: "agent-123",
  data: {
    status: "active",
    cursor: { x: 100, y: 200 },
  },
  userInfo: {
    name: "AI Assistant",
    avatar: "https://example.com/avatar.png",
  },
  ttl: 60, // optional, 2–3599 seconds
});
```

- **userId** (required): The ID of the user to set presence for.
- **data** (required): Presence data as a JSON object.
- **userInfo** (optional): Metadata about the user or agent
- **ttl** (optional): Time-to-live in seconds (minimum 2, maximum 3599).
  Defaults to 60. After this duration, the presence expires automatically.

### Groups

Groups allow you to manage users for group mentions in comments and text
editors.

#### Liveblocks.createGroup [#create-group]

Creates a new group with the specified members. This is a wrapper around the
[Create Group API](/docs/api-reference/rest-api-endpoints#post-groups) and
returns the same response.

```ts
const group = await liveblocks.createGroup({
  groupId: "engineering-team",
  memberIds: ["alice@example.com", "bob@example.com"],
});

// { type: "group", id: "engineering-team", organizationId: "acme-corp", createdAt: "...", updatedAt: "...", scopes: { mention: true }, members: [...] }
console.log(group);
```

You can also create a group without members and add them later:

```ts
const group = await liveblocks.createGroup({
  groupId: "design-team",
  // Optional, add members when creating the group
  memberIds: ["charlie@example.com"],

  // Optional, create it on a specific organization
  organizationId: "company-123",

  // Optional, set group scopes (defaults to { mention: true })
  scopes: { mention: true },
});
```

#### Liveblocks.getGroup [#get-group]

Returns a group by its ID. Throws an error if the group isn’t found. This is a
wrapper around the
[Get Group API](/docs/api-reference/rest-api-endpoints#get-groups-groupId) and
returns the same response.

```ts
const group = await liveblocks.getGroup({
  groupId: "engineering-team",
});

// { type: "group", id: "engineering-team", organizationId: "acme-corp", createdAt: "...", updatedAt: "...", scopes: { mention: true }, members: [...] }
console.log(group);
```

#### Liveblocks.getGroups [#get-groups]

Returns a list of all groups in your project. This is a wrapper around the
[Get Groups API](/docs/api-reference/rest-api-endpoints#get-groups) and returns
the same response.

```ts
const { data: groups, nextCursor } = await liveblocks.getGroups();

// A list of groups
// [{ type: "group", id: "engineering-team", organizationId: "acme-corp", createdAt: "...", updatedAt: "...", scopes: { mention: true }, members: [...] }, ...]
console.log(groups);

// A pagination cursor for the next page
console.log(nextCursor);
```

You can also paginate through groups:

```ts
const { data: groups, nextCursor } = await liveblocks.getGroups({
  // Optional, the number of groups to return (defaults to 20)
  limit: 50,

  // Optional, cursor for pagination
  startingAfter: nextCursor,
});
```

#### Liveblocks.getUserGroups [#get-user-groups]

Returns all groups that a specific user is a member of. This is a wrapper around
the
[Get User Groups API](/docs/api-reference/rest-api-endpoints#get-users-userId-groups)
and returns the same response.

```ts
const { data: userGroups, nextCursor } = await liveblocks.getUserGroups({
  userId: "alice@example.com",
});

// A list of groups the user belongs to
// [{ type: "group", id: "engineering-team", ... }, ...]
console.log(userGroups);
```

You can also paginate through user groups:

```ts
const { data: userGroups, nextCursor } = await liveblocks.getUserGroups({
  userId: "alice@example.com",
  limit: 25,
  startingAfter: "L3YyL2dyb3Vwcy...",
});
```

#### Liveblocks.addGroupMembers [#add-group-members]

Adds new members to an existing group. This is a wrapper around the
[Add Group Members API](/docs/api-reference/rest-api-endpoints#post-groups-groupId-add-members)
and returns the same response.

```ts
const updatedGroup = await liveblocks.addGroupMembers({
  groupId: "engineering-team",
  memberIds: ["david@example.com", "eve@example.com"],
});

// { type: "group", id: "engineering-team", organizationId: "acme-corp", createdAt: "...", updatedAt: "...", scopes: { mention: true }, members: [...] }
console.log(updatedGroup);
```

#### Liveblocks.removeGroupMembers [#remove-group-members]

Removes members from an existing group. This is a wrapper around the
[Remove Group Members API](/docs/api-reference/rest-api-endpoints#post-groups-groupId-remove-members)
and returns the same response.

```ts
const updatedGroup = await liveblocks.removeGroupMembers({
  groupId: "engineering-team",
  memberIds: ["david@example.com"],
});

// { type: "group", id: "engineering-team", organizationId: "acme-corp", createdAt: "...", updatedAt: "...", scopes: { mention: true }, members: [...] }
console.log(updatedGroup);
```

#### Liveblocks.deleteGroup [#delete-group]

Deletes a group. If the group doesn’t exist, no error will be thrown. This is a
wrapper around the
[Delete Group API](/docs/api-reference/rest-api-endpoints#delete-groups-groupId)
and returns no response.

```ts
await liveblocks.deleteGroup({
  groupId: "old-team",
});
```

### Storage

#### Liveblocks.getStorageDocument [#get-rooms-roomId-storage]

Returns the contents of a room’s Storage tree. By default, returns Storage in
LSON format. Throws an error if the room isn’t found. This is a wrapper around
the
[Get Storage Document API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-storage)
and returns the same response.

```ts
const storage = await liveblocks.getStorageDocument("my-room-id");
```

LSON is a custom Liveblocks format that preserves information about the
conflict-free data types used. By default, `getStorageDocument` returns Storage
in this format. This is the same as using `"plain-json"` in the second argument.

```ts highlight="2"
// Retrieve LSON Storage data
const storage = await liveblocks.getStorageDocument("my-room-id", "plain-lson");

// If this were your Storage type...
declare global {
  interface Liveblocks {
    Storage: {
      names: LiveList<string>;
    };
  }
}

// {
//   liveblocksType: "LiveObject",
//   data: {
//     names: {
//       liveblocksType: "LiveList",
//       data: ["Olivier", "Nimesh"],
//     }
//   }
// }
console.log(storage);
```

You can also retrieve Storage as JSON by passing `"json"` into the second
argument.

```ts highlight="2"
// Retrieve JSON Storage data
const storage = await liveblocks.getStorageDocument("my-room-id", "json");

// If this were your Storage type...
declare global {
  interface Liveblocks {
    Storage: {
      names: LiveList<string>;
    };
  }
}

// {
//   names: ["Olivier", "Nimesh"]
// }
console.log(storage);
```

#### Liveblocks.initializeStorageDocument [#post-rooms-roomId-storage]

Initializes a room’s Storage tree with given LSON data. To use this, the room
must have [already been created](#post-rooms) and have empty Storage. Throws an
error if the room isn’t found. Calling this will disconnect all active users
from the room. This is a wrapper around the
[Initialize Storage Document API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-storage)
and returns the same response.

```ts
// Create a new room
const room = await liveblocks.createRoom("my-room-id", {
  defaultAccesses: ["*:write"],
});

// Initialize Storage
const storage = await liveblocks.initializeStorageDocument("my-room-id", {
  // Your LSON Storage value
  // ...
});
```

LSON is a custom Liveblocks format that preserves information about
conflict-free data types. The easiest way to create it is using the
`toPlainLson` helper provided by `@liveblocks/client`. Note that your Storage
root should always be a `LiveObject`.

```ts highlight="11-13,18-20,25"
import { toPlainLson, LiveList, LiveObject } from "@liveblocks/client";

// Create a new room
const room = await liveblocks.createRoom("my-room-id", {
  defaultAccesses: ["*:write"],
});

// If this were your Storage type...
declare global {
  interface Liveblocks {
    Storage: {
      names: LiveList<string>;
    };
  }
}

// Create the initial conflict-free data
const initialStorage: LiveObject<Liveblocks["Storage"]> = new LiveObject({
  names: new LiveList(["Olivier", "Nimesh"]),
});

// Convert to LSON and create Storage
const storage = await liveblocks.initializeStorageDocument(
  "my-room-id",
  toPlainLson(initialStorage)
);
```

It’s also possible to create plain LSON manually, without the helper function.

```ts highlight="9-11,17-23"
// Create a new room
const room = await liveblocks.createRoom("my-room-id", {
  defaultAccesses: ["*:write"],
});

// If this were your Storage type...
declare global {
  interface Liveblocks {
    Storage: {
      names: LiveList<string>;
    };
  }
}

// Create this Storage and add names to the LiveList
const storage = await liveblocks.initializeStorageDocument("my-room-id", {
  liveblocksType: "LiveObject",
  data: {
    names: {
      liveblocksType: "LiveList",
      data: ["Olivier", "Nimesh"],
    },
  },
});
```

#### Liveblocks.mutateStorage [#mutate-storage]

Modify Storage contents from the server. No presence will be shown when you make
changes.

```ts
// Mutate a single room
await liveblocks.mutateStorage(
  "my-room-id",

  ({ root }) => {
    root.get("list").push("item3");
  }
);
```

The callback can be asynchronous, in which case a stream of mutations can happen
over time.

```ts
// Mutate a single room
await liveblocks.mutateStorage(
  "my-room-id",

  async ({ root }) => {
    // These changes happen immediately
    const animals = root.get("animals");
    animals.clear();
    animals.push("Thinking...");

    await thinkForAWhile();

    // These changes happen after `await` has run
    animals.clear();
    animals.push("🐶");
    animals.push("🦘");
  }
);
```

Learn how to
[type your Storage](/docs/api-reference/liveblocks-react#Typing-your-data).

#### Liveblocks.massMutateStorage [#mass-mutate-storage]

Modify Storage contents for multiple rooms simultaneously. With the default
query value `{}` it will loop through every room in your project.

```ts
// Mutate a number of rooms
await liveblocks.massMutateStorage(
  {},

  // Callback runs on every selected room
  ({ room, root }) => {
    // { type: "room", id: "my-room-id", metadata: {...}, ... }
    console.log(room);

    root.get("animals").push("🦍");
  }
);
```

A number of options are also available, enabling you to filter for certain
rooms. Additionally, you can set options for concurrency and provide an abort
signal to cancel the mutations.

```ts
// Mutate a number of rooms
await liveblocks.massMutateStorage(
  {
    // +++
    // Optional, filter for rooms that allow entry to group ID(s) in `groupsAccesses`
    groupIds: ["engineering", "design"],

    // Optional, filter for rooms that allow entry to a user's ID in `usersAccesses`
    userId: "my-user-id",

    // Optional, use advanced filtering
    query: {
      // Optional, filter for rooms with an ID that starts with specific string
      roomId: {
        startsWith: "liveblocks:",
      },
      // Optional, filter for rooms with custom metadata in `metadata`
      metadata: {
        roomType: "whiteboard",
      },
    },
    // +++
  },

  ({ room, root }) => {
    // { type: "room", id: "my-room-id", metadata: {...}, ... }
    console.log(room);

    root.get("animals").push("🦍");
  },

  // Optional
  // +++
  {
    concurrency: 10, // Optional, process at most 10 rooms simultaneously
    signal, // Optional, provide an abort signal to cancel mutations mid-way
  }
  // +++
);
```

Learn how to
[type your Storage](/docs/api-reference/liveblocks-react#Typing-your-data).

#### Liveblocks.deleteStorageDocument [#delete-rooms-roomId-storage]

Deletes a room’s Storage data. Calling this will disconnect all active users
from the room. Throws an error if the room isn’t found. This is a wrapper around
the
[Delete Storage Document API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-storage)
and returns no response.

```ts
await liveblocks.deleteStorageDocument("my-room-id");
```

#### Liveblocks.uploadFile [#upload-file]

Uploads a file to a room and returns a
[`LiveFile`](/docs/api-reference/liveblocks-client#LiveFile) which can then be
added to the room's Storage tree.

```ts
const liveFile = await liveblocks.uploadFile({
  roomId: "my-room-id",
  file,
});
```

Pass an `AbortSignal` in the optional second argument to cancel the upload.

```ts
const liveFile = await liveblocks.uploadFile(
  {
    roomId: "my-room-id",
    file,
  },
  { signal }
);
```

#### Liveblocks.getFileUrl [#get-file-url]

Returns a presigned URL and its expiration time for a
[`LiveFile`](/docs/api-reference/liveblocks-client#LiveFile). You can pass a
`LiveFile`, its `LiveFileData`, or its ID. Throws an error if the room or file
isn't found.

```ts
const { url, expiresAt } = await liveblocks.getFileUrl({
  roomId: "my-room-id",
  file: liveFile,
});
```

Returns a `StorageFileUrl` object with the following properties:

<PropertiesList>
  <PropertiesListItem name="url" type="string">
    A presigned URL for the file.
  </PropertiesListItem>
  <PropertiesListItem name="expiresAt" type="string">
    The expiration time of the presigned URL.
  </PropertiesListItem>
</PropertiesList>

Pass an `AbortSignal` in the optional second argument to cancel the request.

### Yjs

#### Liveblocks.getYjsDocument [#get-rooms-roomId-ydoc]

Returns a JSON representation of a room’s Yjs document. Throws an error if the
room isn’t found. This is a wrapper around the
[Get Yjs Document API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-ydoc)
and returns the same response.

```ts
const yjsDocument = await liveblocks.getYjsDocument("my-room-id");

// { yourYText: "...", yourYArray: [...], ... }
console.log(yjsDocument);
```

A number of options are available.

```ts
const yjsDocument = await liveblocks.getYjsDocument("my-room-id", {
  // Optional, if true, `yText` values will return formatting
  format: true,

  // Optional, return a single key's value, e.g. `yDoc.get("my-key-id").toJson()`
  key: "my-key-id",

  // Optional, override the inferred `key` type, e.g. "ymap" for `doc.get(key, Y.Map)`
  type: "ymap",
});
```

#### Liveblocks.sendYjsBinaryUpdate [#put-rooms-roomId-ydoc]

Send a Yjs binary update to a room’s Yjs document. You can use this to update or
initialize the room’s Yjs document. Throws an error if the room isn’t found.
This is a wrapper around the
[Send a Binary Yjs Update API](/docs/api-reference/rest-api-endpoints#put-rooms-roomId-ydoc)
and returns no response.

```ts
await liveblocks.sendYjsBinaryUpdate("my-room-id", update);
```

Here’s an example of how to update a room’s Yjs document with your changes.

```ts
import * as Y from "yjs";

// Create a Yjs document
const yDoc = new Y.Doc();

// Create your data structures and make your update
// If you're using a text editor, you need to match its format
const yText = yDoc.getText("text");
yText.insert(0, "Hello world");

// Encode the document state as an update
const update = Y.encodeStateAsUpdate(yDoc);

// Send update to Liveblocks
await liveblocks.sendYjsBinaryUpdate("my-room-id", update);
```

To update a subdocument instead of the main document, pass its `guid`.

```ts
await liveblocks.sendYjsBinaryUpdate("my-room-id", update, {
  // Optional, update a subdocument instead. guid is its unique identifier
  guid: "c4a755...",
});
```

To create a new room and initialize its Yjs document, call
[`liveblocks.createRoom`](#post-rooms) before sending the binary update.

```ts highlight="1-2"
// Create new room
const room = await liveblocks.createRoom("my-room-id");

// Set initial Yjs document value
await liveblocks.sendYjsBinaryUpdate("my-room-id", state);
```

##### Different editors

Note that each text and code editor handles binary updates in a different way,
and may use a different Yjs shared type, for example
[`Y.XmlFragment`](https://docs.yjs.dev/api/shared-types/y.xmlfragment) instead
of [`Y.Text`](https://docs.yjs.dev/api/shared-types/y.text).

Create a binary update with [Slate](https://www.slatejs.org/):

```ts title="Slate binary update" highlight="3,13-17,19-21" isCollapsed isCollapsable
import { Liveblocks } from "@liveblocks/node";
import * as Y from "yjs";
import { slateNodesToInsertDelta } from "@slate-yjs/core";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function POST() {
  // Create a Yjs document
  const yDoc = new Y.Doc();

  // Create Slate document state
  const slateDoc = {
    type: "paragraph",
    children: [{ text: "Hello world" }],
  };

  // Create your data structures and make your update
  const insertDelta = slateNodesToInsertDelta(slateDoc);
  (yDoc.get("content", Y.XmlText) as Y.XmlText).applyDelta(insertDelta);

  // Encode the document state as an update
  const update = Y.encodeStateAsUpdate(yDoc);

  // Send update to Liveblocks
  await liveblocks.sendYjsBinaryUpdate("my-room-id", update);
}
```

Create a binary update with
[Tiptap](https://tiptap.dev/docs/editor/api/extensions/collaboration):

```ts title="Tiptap binary update" highlight="12-14,16-18" isCollapsed isCollapsable
import * as Y from "yjs";
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function POST() {
  // Create a Yjs document
  const yDoc = new Y.Doc();

  // Create Tiptap Yjs state
  const yXmlElement = new Y.XmlElement("paragraph");
  yXmlElement.insert(0, [new Y.XmlText("Hello world")]);

  // Create your data structures and make your update
  const yXmlFragment = yDoc.getXmlFragment("default");
  yXmlFragment.insert(0, [yXmlElement]);

  // Encode the document state as an update message
  const yUpdate = Y.encodeStateAsUpdate(yDoc);

  // Initialize the Yjs document with the update
  await liveblocks.sendYjsBinaryUpdate("my-room-id", {
    update: yUpdate,
  });
}
```

Read the [Yjs documentation](https://docs.yjs.dev/api/document-updates) to learn
more about creating binary updates.

#### Liveblocks.getYjsDocumentAsBinaryUpdate [#get-rooms-roomId-ydoc-binary]

Return a room’s Yjs document as a single binary update. You can use this to get
a copy of your Yjs document in your back end. Throws an error if the room isn’t
found. This is a wrapper around the
[Get Yjs Document Encoded as a Binary Yjs Update API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-ydoc-binary)
and returns the same response.

```ts
const binaryYjsUpdate =
  await liveblocks.getYjsDocumentAsBinaryUpdate("my-room-id");
```

To return a subdocument instead of the main document, pass its `guid`.

```ts
const binaryYjsUpdate = await liveblocks.getYjsDocumentAsBinaryUpdate(
  "my-room-id",
  {
    // Optional, return a subdocument instead. guid is its unique identifier
    guid: "c4a755...",
  }
);
```

Read the [Yjs documentation](https://docs.yjs.dev/api/document-updates) to learn
more about using binary updates.

### Version History

#### Liveblocks.getVersionHistory [#get-version-history]

Returns a room’s version history snapshots, sorted by creation date from newest
to oldest. Throws an error if the room isn’t found. This is a wrapper around the
[Get Version History API](/docs/api-reference/rest-api-endpoints#get-version-history)
and returns the same response.

```ts
const { data: versions, nextCursor } = await liveblocks.getVersionHistory(
  "my-room-id",
  {
    // Optional, defaults to 20
    limit: 20,

    // Optional, used for pagination
    cursor: "eyJjcmVhdGVkQXQi...",
  }
);
```

#### Liveblocks.createVersionHistorySnapshot [#create-version-history-snapshot]

Creates a new version history snapshot of a room, capturing both its Storage and
Yjs documents. Throws an error if the room isn’t found. This is a wrapper around
the
[Create Version History Snapshot API](/docs/api-reference/rest-api-endpoints#create-version-history-snapshot)
and returns the same response.

```ts
const { data } = await liveblocks.createVersionHistorySnapshot("my-room-id");

// { id: "vh_d75sF3..." }
console.log(data);
```

#### Liveblocks.getYjsVersion [#get-yjs-version]

Returns a specific version of a room’s Yjs document encoded as a binary Yjs
update. Throws an error if the room or version isn’t found. This is a wrapper
around the
[Get Yjs Document Version API](/docs/api-reference/rest-api-endpoints#get-yjs-version)
and returns the same response.

```ts
const binaryYjsUpdate = await liveblocks.getYjsVersion({
  roomId: "my-room-id",
  versionId: "vh_d75sF3...",
});
```

#### Liveblocks.deleteVersion [#delete-version]

Permanently deletes a version from a room’s history. Throws an error if the room
or version isn’t found. This is a wrapper around the
[Delete Version API](/docs/api-reference/rest-api-endpoints#delete-version) and
returns no response.

```ts
await liveblocks.deleteVersion({
  roomId: "my-room-id",
  versionId: "vh_d75sF3...",
});
```

### Attachments

#### Liveblocks.uploadAttachment [#upload-attachment]

Uploads a file to a room and returns a `CommentAttachment` which can be added to
a comment. The `userId` must match the comment's user ID.

```ts
const attachment = await liveblocks.uploadAttachment({
  roomId: "my-room-id",
  userId: "pierre@example.com",
  file,
});

const comment = await liveblocks.createComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  data: {
    userId: "pierre@example.com",
    body,
    // +++
    attachmentIds: [attachment.id],
    // +++
  },
});
```

You can also pass uploaded attachment IDs to `data.comment.attachmentIds` in
[`createThread`](#post-rooms-roomId-threads), or to `data.attachmentIds` in
[`editComment`](#post-rooms-roomId-threads-threadId-comments-commentId). When
editing, pass the ID of every attachment that should remain on the comment; pass
an empty array to remove them all. A comment can have up to 10 attachments.

Pass an `AbortSignal` in the optional second argument to cancel the upload.

```ts
const attachment = await liveblocks.uploadAttachment(
  {
    roomId: "my-room-id",
    userId: "pierre@example.com",
    file,
  },
  { signal }
);
```

#### Liveblocks.getAttachment [#get-rooms-roomId-attachments-attachmentId]

Returns an attachment's metadata and a presigned download URL. Throws an error
if the room or attachment isn't found.

```ts
const attachment = await liveblocks.getAttachment({
  roomId: "my-room-id",
  attachmentId: "at_d75sF3...",
});

// { type: "attachment", id: "at_d75sF3...", name: "document.pdf", ... }
console.log(attachment);

// The presigned URL to download the attachment
console.log(attachment.url);
```

Returns an `AttachmentWithUrl` object with the following properties:

<PropertiesList>
  <PropertiesListItem name="type" type={'"attachment"'}>
    The type of the object.
  </PropertiesListItem>
  <PropertiesListItem name="id" type="string">
    The attachment ID (starts with "at_").
  </PropertiesListItem>
  <PropertiesListItem name="name" type="string">
    The name of the attachment file.
  </PropertiesListItem>
  <PropertiesListItem name="mimeType" type="string">
    The MIME type of the attachment.
  </PropertiesListItem>
  <PropertiesListItem name="size" type="number">
    The size of the attachment in bytes.
  </PropertiesListItem>
  <PropertiesListItem name="url" type="string">
    A presigned URL to download the attachment.
  </PropertiesListItem>
  <PropertiesListItem name="expiresAt" type="string">
    The expiration time of the presigned URL.
  </PropertiesListItem>
</PropertiesList>

### Comments

#### Liveblocks.getThreads [#get-rooms-roomId-threads]

Returns a list of threads found inside a room. Throws an error if the room isn’t
found. This is a wrapper around the
[Get Room Threads API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads)
and returns the same response.

```ts
const { data: threads } = await liveblocks.getThreads({
  roomId: "my-room-id",
});

// [{ type: "thread", id: "th_d75sF3...", ... }, ...]
console.log(threads);
```

It’s also possible to filter threads by visibility, resolved status, and their
string, boolean, and number metadata using a query parameter. You can also pass
`startsWith` to match the start of a string.

```ts
const { data: threads } = await liveblocks.getThreads({
  roomId: "my-room-id",

  // Optional, use advanced filtering
  query: {
    // Optional, filter based on resolved status
    resolved: false,

    // Optional, filter based on visibility
    visibility: "private",

    // Optional, filter for metadata values
    metadata: {
      status: "open",
      pinned: true,
      priority: 3,

      // You can match the start of a metadata string
      organization: {
        startsWith: "liveblocks:",
      },
    },
  },
});
```

You can also pass a
[query language](/docs/guides/how-to-filter-threads-using-query-language) string
instead of a `query` object.

#### Liveblocks.createThread [#post-rooms-roomId-threads]

Creates a new thread within a specific room, using room ID and thread data.
Threads are public by default, but can be created as private by passing
`visibility: "private"`. This is a wrapper around the
[Create Thread API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads)
and returns the new thread.

```ts
const thread = await liveblocks.createThread({
  roomId: "my-room-id",

  data: {
    comment: {
      userId: "florent@example.com",
      body: {
        version: 1,
        content: [
          /* The comment's body text goes here, see below */
        ],
      },
    },
  },
});

// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```

A comment’s body is an array of paragraphs, each containing child nodes. Here’s
an example of how to construct a comment’s body, which can be submitted under
`data.comment.body`.

```tsx highlight="3-11,20"
import { CommentBody } from "@liveblocks/node";

const body: CommentBody = {
  version: 1,
  content: [
    {
      type: "paragraph",
      children: [{ text: "Hello " }, { text: "world", bold: true }],
    },
  ],
};

const thread = await liveblocks.createThread({
  roomId: "my-room-id",

  data: {
    // ...
    comment: {
      // The comment's body, uses the `CommentBody` type
      body,

      // ...
    },
  },
});
```

<Banner title="Creating a comment from Markdown">

You can also convert a Markdown string to a `CommentBody` with
[`markdownToCommentBody`](#markdown-to-comment-body).

</Banner>

This method has a number of options, allowing for custom metadata, thread
visibility, and a creation date for the comment.

```ts
const thread = await liveblocks.createThread({
  roomId: "my-room-id",

  data: {
    // Optional, custom metadata properties
    metadata: {
      color: "blue",
      page: 3,
      pinned: true,
    },

    // Optional, defaults to "public"
    visibility: "private",

    // Data for the first comment in the thread
    comment: {
      // The ID of the user that created the comment
      userId: "florent@example.com",

      // Optional, when the comment was created.
      createdAt: new Date(),

      // Optional, custom comment metadata
      metadata: {
        tag: "important",
        spam: false,
      },

      // Optional, IDs of uploaded comment attachments
      attachmentIds: [attachment.id],

      // The comment's body, uses the `CommentBody` type
      body: {
        version: 1,
        content: [
          /* The comment's body text goes here, see above */
        ],
      },
    },
  },
});

// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```

#### Liveblocks.getThread [#get-rooms-roomId-threads-threadId]

Returns a thread. Throws an error if the room or thread isn’t found. This is a
wrapper around the
[Get Thread API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId)
and returns the same response.

```ts
const thread = await liveblocks.getThread({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
});

// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```

#### Liveblocks.editThreadMetadata [#post-rooms-roomId-threads-threadId-metadata]

Updates the metadata of a specific thread within a room. This method allows you
to modify the metadata of a thread, including user information and the date of
the last update. Throws an error if the room or thread isn’t found. This is a
wrapper around the
[Update Thread Metadata API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-metadata)
and returns the updated metadata.

```ts
const editedMetadata = await liveblocks.editThreadMetadata({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",

  data: {
    metadata: {
      color: "yellow",
    },
    userId: "marc@example.com",
    updatedAt: new Date(), // Optional
  },
});

// { color: "yellow", page: 3, pinned: true }
console.log(editedMetadata);
```

Metadata can be a `string`, `number`, or `boolean`. You can also use `null` to
remove metadata from a thread. Here’s an example using every option.

```ts
const editedMetadata = await liveblocks.editThreadMetadata({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",

  data: {
    // Custom metadata
    metadata: {
      // Metadata can be a string, number, or boolean
      title: "My thread title",
      page: 3,
      pinned: true,

      // Remove metadata with null
      color: null,
    },

    // The ID of the user that updated the metadata
    userId: "marc@example.com",

    // Optional, the time the user updated the metadata
    updatedAt: new Date(),
  },
});

// { title: "My thread title", page: 3, pinned: true }
console.log(editedMetadata);
```

#### Liveblocks.editCommentMetadata [#post-rooms-roomId-threads-threadId-comments-commentId-metadata]

Updates the metadata of a specific comment within a thread. This method allows
you to modify the metadata of a comment, including user information and the date
of the last update. Throws an error if the room, thread, or comment isn’t found.
This is a wrapper around the
[Update Comment Metadata API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-metadata)
and returns the updated metadata.

```ts
const editedMetadata = await liveblocks.editCommentMetadata({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    metadata: {
      spam: false,
    },
    userId: "stacy@example.com",
    updatedAt: new Date(), // Optional
  },
});

// { spam: false }
console.log(editedMetadata);
```

Metadata can be a `string`, `number`, or `boolean`. You can also use `null` to
remove metadata from a comment. Here’s an example using every option.

```ts
const editedMetadata = await liveblocks.editCommentMetadata({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    // Custom metadata
    metadata: {
      // Metadata can be a string, number, or boolean
      tag: "important",
      priority: 2,
      spam: true,

      // Remove metadata with null
      assignedTo: null,
    },

    // The ID of the user that updated the metadata
    userId: "stacy@example.com",

    // Optional, the time the user updated the metadata
    updatedAt: new Date(),
  },
});

// { tag: "important", priority: 2, flagged: true }
console.log(editedMetadata);
```

#### Liveblocks.markThreadAsResolved [#post-rooms-roomId-threads-threadId-mark-as-resolved]

Marks a thread as resolved, which means it sets the `resolved` property on the
specified thread to `true`. Takes a `userId`, which is the ID of the user that
resolved the thread. Throws an error if the room or thread isn’t found. This is
a wrapper around the
[Mark Thread As Resolved API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-resolved)
and returns the same response.

```ts
const thread = await liveblocks.markThreadAsResolved({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  data: {
    userId: "steven@example.com",
  },
});

// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```

#### Liveblocks.markThreadAsUnresolved [#post-rooms-roomId-threads-threadId-mark-as-unresolved]

Marks a thread as unresolved, which means it sets the `resolved` property on the
specified thread to `false`. Takes a `userId`, which is the ID of the user that
unresolved the thread. Throws an error if the room or thread isn’t found. This
is a wrapper around the
[Mark Thread As Unresolved API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-unresolved)
and returns the same response.

```ts
const thread = await liveblocks.markThreadAsUnresolved({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  data: {
    userId: "steven@example.com",
  },
});

// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```

#### Liveblocks.deleteThread [#delete-rooms-roomId-threads-threadId]

Deletes a thread. Throws an error if the room or thread isn’t found. This is a
wrapper around the
[Delete Thread API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-threads-threadId)
and returns no response.

```ts
await liveblocks.deleteThread({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
});
```

#### Liveblocks.subscribeToThread [#post-rooms-roomId-threads-threadId-subscribe]

Subscribes a user to a thread, meaning they will receive inbox notifications
when new comments are posted. Throws an error if the room or thread isn’t found.
This is a wrapper around the
[Subscribe To Thread API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-subscribe)
and returns the same response.

```ts
const subscription = await liveblocks.subscribeToThread({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  data: {
    userId: "steven@example.com",
  },
});

// { kind: "thread", subjectId: "th_d75sF3...", ... }
console.log(subscription);
```

Subscribing will replace any existing subscription for the current thread
[set at room-level](#post-rooms-roomId-users-userId-subscription-settings). This
value can also be overridden by a room-level call that is run afterwards.

```ts
const roomId = "my-room-id";
const userId = "steven@example.com";

// 1. Disables notifications for all threads
await liveblocks.updateRoomSubscriptionSettings({
  roomId,
  userId,
  data: {
    threads: "none",
  },
});

// 2. Enables notifications just for this thread, "th_d75sF3..."
await liveblocks.subscribeToThread({
  roomId,
  threadId: "th_d75sF3...",
  data: { userId },
});

// 3. Disables notifications for all threads, including "th_d75sF3..."
await liveblocks.updateRoomSubscriptionSettings({
  roomId,
  userId,
  data: {
    threads: "none",
  },
});
```

#### Liveblocks.unsubscribeFromThread [#post-rooms-roomId-threads-threadId-unsubscribe]

Unsubscribes a user from a thread, meaning they will no longer receive inbox
notifications when new comments are posted. Throws an error if the room or
thread isn’t found. This is a wrapper around the
[Unsubscribe From Thread API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-unsubscribe)
and returns the same response.

```ts
await liveblocks.unsubscribeFromThread({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  data: {
    userId: "steven@example.com",
  },
});
```

Unsubscribing will replace any existing subscription for the current thread
[set at room-level](#post-rooms-roomId-users-userId-subscription-settings). This
value can also be overridden by a room-level call that is run afterwards.

```ts
const roomId = "my-room-id";
const userId = "steven@example.com";

// 1. Enables notifications for all thread activity
await liveblocks.updateRoomSubscriptionSettings({
  roomId,
  userId,
  data: {
    threads: "all",
  },
});

// 2. Disables notifications just for this thread, "th_d75sF3..."
await liveblocks.unsubscribeFromThread({
  roomId,
  threadId: "th_d75sF3...",
  data: { userId },
});

// 3. Enables notifications for all thread activity, including "th_d75sF3..."
await liveblocks.updateRoomSubscriptionSettings({
  roomId,
  userId,
  data: {
    threads: "none",
  },
});
```

#### Liveblocks.getThreadSubscriptions [#get-rooms-roomId-threads-threadId-subscriptions]

Gets a thread’s subscriptions, returning a list of users that will receive
notifications when new comments are posted. Throws an error if the room or
thread isn’t found. This is a wrapper around the
[Get Thread Subscriptions API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId-subscriptions)
and returns the same response.

```ts
const { data: subscriptions } = await liveblocks.getThreadSubscriptions({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
});

// [{ kind: "thread", subjectId: "th_d75sF3...", userId: "steven@example.com", ... }, ...]
console.log(subscriptions);
```

#### Liveblocks.createComment [#post-rooms-roomId-threads-threadId-comments]

Creates a new comment in a specific thread within a room. This method allows
users to add comments to a conversation thread, specifying the user who made the
comment and the content of the comment. This method is a wrapper around the
[Create Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments)
and returns the new comment.

```ts
const comment = await liveblocks.createComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",

  data: {
    body: {
      version: 1,
      content: [
        /* The comment's body text goes here, see below */
      ],
    },
    userId: "pierre@example.com",
    createdAt: new Date(), // Optional
  },
});
```

A comment’s body is an array of paragraphs, each containing child nodes. Here’s
an example of how to construct a comment’s body, which can be submitted under
`data.body`.

```tsx highlight="3-11,19"
import { CommentBody } from "@liveblocks/node";

const body: CommentBody = {
  version: 1,
  content: [
    {
      type: "paragraph",
      children: [{ text: "Hello " }, { text: "world", bold: true }],
    },
  ],
};

const comment = await liveblocks.createComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",

  data: {
    // The comment's body, uses the `CommentBody` type
    body,

    // ...
  },
});
```

<Banner title="Creating a comment from Markdown">

You can also convert a Markdown string to a `CommentBody` with
[`markdownToCommentBody`](#markdown-to-comment-body).

</Banner>

This method has a number of options, including the option to add a custom
creation date and metadata to the comment.

```ts
const comment = await liveblocks.createComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",

  data: {
    // The comment's body, uses the `CommentBody` type
    body: {
      version: 1,
      content: [
        /* The comment's body text goes here, see above */
      ],
    },

    // The ID of the user that created the comment
    userId: "adrien@example.com",

    // Optional, the time the comment was created
    createdAt: new Date(),

    // Optional, custom comment metadata
    metadata: {
      tag: "important",
      reviewed: false,
    },

    // Optional, IDs of uploaded comment attachments
    attachmentIds: [attachment.id],
  },
});
```

#### Liveblocks.getComment [#get-rooms-roomId-threads-threadId-comments-commentId]

Returns a comment. Throws an error if the room, thread, or comment isn’t found.
This is a wrapper around the
[Get Comment API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId-comments-commentId)
and returns the same response.

```ts
const comment = await liveblocks.getComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",
});

// { type: "comment", threadId: "th_d75sF3...", ... }
console.log(comment);
```

#### Liveblocks.editComment [#post-rooms-roomId-threads-threadId-comments-commentId]

Edits an existing comment in a specific thread within a room. This method allows
users to update the content of their previously posted comments, with the option
to specify the time of the edit. Throws an error if the comment isn’t found.
This is a wrapper around the
[Edit Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId)
and returns the updated comment.

```ts
const editedComment = await liveblocks.editComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    body: {
      version: 1,
      content: [
        /* The comment's body text goes here, see below */
      ],
    },

    // Optional, the time the comment was edited
    editedAt: new Date(),

    // Optional, custom comment metadata
    metadata: {
      tag: "important",
      spam: false,
    },

    // Optional, IDs of every attachment that should remain on the comment
    attachmentIds: [attachment.id],
  },
});

// { type: "comment", threadId: "th_d75sF3...", ... }
console.log(editedComment);
```

A comment’s body is an array of paragraphs, each containing child nodes. Here’s
an example of how to construct a comment’s body, which can be submitted under
`data.body`.

```tsx
import { CommentBody } from "@liveblocks/node";

// +++
const body: CommentBody = {
  version: 1,
  content: [
    {
      type: "paragraph",
      children: [{ text: "Hello " }, { text: "world", bold: true }],
    },
  ],
};
// +++

const editedComment = await liveblocks.editComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    // The comment's body, uses the `CommentBody` type
    // +++
    body,
    // +++

    // ...
  },
});
```

<Banner title="Creating a comment from Markdown">

You can also convert a Markdown string to a `CommentBody` with
[`markdownToCommentBody`](#markdown-to-comment-body).

</Banner>

#### Liveblocks.deleteComment [#delete-rooms-roomId-threads-threadId-comments-commentId]

Deletes a specific comment from a thread within a room. If there are no
remaining comments in the thread, the thread is also deleted. This method throws
an error if the comment isn’t found. This is a wrapper around the
[Delete Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId)
and returns no response.

```ts
await liveblocks.deleteComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",
});
```

#### Liveblocks.addCommentReaction [#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction]

Adds a reaction to a specific comment in a thread within a room. Throws an error
if the comment isn’t found or if the user has already added the same reaction on
the comment. This is a wrapper around the
[Add Comment Reaction API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction)
and returns the new reaction.

```ts
const reaction = await liveblocks.addCommentReaction({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    emoji: "👨‍👩‍👧",
    userId: "guillaume@example.com",
    createdAt: new Date(), // Optional, the time the reaction was added
  },
});

// { emoji: "👨‍👩‍👧", userId "guillaume@example.com", ... }
console.log(reaction);
```

#### Liveblocks.removeCommentReaction [#post-rooms-roomId-threads-threadId-comments-commentId-remove-reaction]

Removes a reaction from a specific comment in a thread within a room. Throws an
error if the comment reaction isn’t found. This is a wrapper around the
[Remove Comment Reaction API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-remove-reaction)
and returns no response.

```ts
await liveblocks.removeCommentReaction({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    emoji: "👨‍👩‍👧",
    userId: "steven@example.com",
    removedAt: new Date(), // Optional, the time the reaction is to be removed
  },
});
```

#### Liveblocks.getRoomSubscriptionSettings [#get-rooms-roomId-users-userId-subscription-settings]

Returns a user’s subscription settings for a specific room, specifying which
`thread` and `textMention` inbox notifications they are set to receive. This is
a wrapper around the
[Get Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-users-userId-subscription-settings).

```ts
const subscriptionSettings = await liveblocks.getRoomSubscriptionSettings({
  roomId: "my-room-id",
  userId: "steven@example.com",
});

// { threads: "all", textMentions: "mine" }
console.log(subscriptionSettings);
```

For `"threads"`, these are the three possible values:

- `"all"` Receive notifications for every activity in every thread.
- `"replies_and_mentions"` Receive notifications for mentions and threads you’re
  participating in.
- `"none"` No notifications are received.

For `"textMentions"`, these are the two possible values:

- `"mine"` Receive notifications for mentions of you.
- `"none"` No notifications are received.

#### Liveblocks.updateRoomSubscriptionSettings [#post-rooms-roomId-users-userId-subscription-settings]

Updates a user’s subscription settings for a specific room, defining which
`thread` and `textMention` inbox notifications they will receive. This is a
wrapper around the
[Update Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-users-userId-subscription-settings).

```ts
const updatedSubscriptionSettings =
  await liveblocks.updateRoomSubscriptionSettings({
    roomId: "my-room-id",
    userId: "steven@example.com",
    data: {
      threads: "replies_and_mentions",
      textMentions: "mine",
    },
  });

// { threads: "replies_and_mentions", ... }
console.log(updatedSubscriptionSettings);
```

For `"threads"`, these are the three possible values that can be set:

- `"all"` Receive notifications for every activity in every thread.
- `"replies_and_mentions"` Receive notifications for mentions and threads you’re
  participating in.
- `"none"` No notifications are received.

For `"textMentions"`, these are the two possible values that can be set:

- `"mine"` Receive notifications for mentions of you.
- `"none"` No notifications are received.

##### Replacing individual thread subscriptions

Subscribing will replace any
[existing thread subscriptions](#post-rooms-roomId-users-userId-subscription-settings)
in the current room. This value can also be overridden by a room-level call that
is run afterwards.

```ts
const roomId = "my-room-id";
const userId = "steven@example.com";

// 1. Enables notifications just for this thread, "th_d75sF3..."
await liveblocks.subscribeToThread({
  roomId,
  threadId: "th_d75sF3...",
  data: { userId },
});

// 2. Disables notifications for all threads, including "th_d75sF3..."
await liveblocks.updateRoomSubscriptionSettings({
  roomId,
  userId,
  data: {
    threads: "none",
  },
});
```

#### Liveblocks.deleteRoomSubscriptionSettings [#delete-rooms-roomId-users-userId-subscription-settings]

Deletes a user’s subscription settings for a specific room. This is a wrapper
around the
[Delete Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-users-userId-subscription-settings).

```ts
await liveblocks.deleteRoomSubscriptionSettings({
  roomId: "my-room-id",
  userId: "steven@example.com",
});
```

#### Liveblocks.getUserRoomSubscriptionSettings [#get-users-userId-room-subscription-settings]

Returns a list of a user’s subscription settings for all rooms. This is a
wrapper around the
[Get User Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#get-users-userId-room-subscription-settings).

```ts
const { data: subscriptionSettings, nextCursor } =
  await liveblocks.getUserRoomSubscriptionSettings({
    userId: "steven@example.com",

    // Optional, filter for a specific organization
    organizationId: "acme-corp",
  });

console.log(subscriptionSettings);

// { roomId: "my-room-id", threads: "all", ... }

// Pagination
if (nextCursor) {
  const { data: nextPage } = await liveblocks.getUserRoomSubscriptionSettings({
    userId: "steven@example.com",
    startingAfter: nextCursor,
  });
}
```

### Feeds

#### Liveblocks.getFeeds [#get-rooms-roomId-feeds]

Returns a list of feeds in a room. This is a wrapper around the
[Get Room Feeds API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-feeds)
and returns the same response.

```ts
const { data: feeds } = await liveblocks.getFeeds({
  roomId: "my-room-id",
});

// [{ feedId: "feed-1", metadata: {...}, timestamp: 1234567890 }, ...]
console.log(feeds);
```

#### Liveblocks.createFeed [#post-rooms-roomId-feed]

Creates a new feed in a room. This is a wrapper around the
[Create Feed API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-feed)
and returns the created feed.

```ts
const feed = await liveblocks.createFeed({
  roomId: "my-room-id",
  feedId: "my-feed-id",

  // Optional, custom metadata for the feed
  metadata: {
    name: "My Feed",
    channel: true,
  },

  // Optional, timestamp in milliseconds. Defaults to current time if not provided
  timestamp: Date.now(),
});

// { feedId: "my-feed-id", metadata: {...}, timestamp: 1234567890 }
console.log(feed);
```

#### Liveblocks.getFeed [#get-rooms-roomId-feeds-feedId]

Returns a feed by its ID. This is a wrapper around the
[Get Feed API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-feeds-feedId)
and returns the same response.

```ts
const feed = await liveblocks.getFeed({
  roomId: "my-room-id",
  feedId: "my-feed-id",
});

// { feedId: "my-feed-id", metadata: {...}, timestamp: 1234567890 }
console.log(feed);
```

#### Liveblocks.updateFeed [#patch-rooms-roomId-feeds-feedId]

Updates the metadata of a feed. This is a wrapper around the
[Update Feed API](/docs/api-reference/rest-api-endpoints#patch-rooms-roomId-feeds-feedId).

```ts
await liveblocks.updateFeed({
  roomId: "my-room-id",
  feedId: "my-feed-id",
  metadata: {
    name: "Updated Feed Name",
    updated: new Date().toISOString(),
  },
});
```

#### Liveblocks.deleteFeed [#delete-rooms-roomId-feeds-feedId]

Deletes a feed. This is a wrapper around the
[Delete Feed API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-feeds-feedId).

```ts
await liveblocks.deleteFeed({
  roomId: "my-room-id",
  feedId: "my-feed-id",
});
```

#### Liveblocks.getFeedMessages [#get-rooms-roomId-feeds-feedId-messages]

Returns a list of messages in a feed. This is a wrapper around the
[Get Feed Messages API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-feeds-feedId-messages)
and returns the same response.

```ts
const { data: messages } = await liveblocks.getFeedMessages({
  roomId: "my-room-id",
  feedId: "my-feed-id",
});

// [{ id: "msg-1", timestamp: 1234567890, data: {...} }, ...]
console.log(messages);
```

#### Liveblocks.createFeedMessage [#post-rooms-roomId-feeds-feedId-messages]

Creates a new message in a feed. This is a wrapper around the
[Create Feed Message API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-feeds-feedId-messages)
and returns the created message.

```ts
const message = await liveblocks.createFeedMessage({
  roomId: "my-room-id",
  feedId: "my-feed-id",

  // The message data
  data: {
    role: "user",
    content: "Hello, world!",
  },

  // Optional, custom message ID. One will be generated if not provided
  id: "my-message-id",

  // Optional, timestamp in milliseconds. Defaults to current time if not provided
  timestamp: Date.now(),
});

// { id: "my-message-id", timestamp: 1234567890, data: {...} }
console.log(message);
```

#### Liveblocks.updateFeedMessage [#patch-rooms-roomId-feeds-feedId-messages-messageId]

Updates a feed message. This is a wrapper around the
[Update Feed Message API](/docs/api-reference/rest-api-endpoints#patch-rooms-roomId-feeds-feedId-messages-messageId).

```ts
await liveblocks.updateFeedMessage({
  roomId: "my-room-id",
  feedId: "my-feed-id",
  messageId: "my-message-id",

  // The message data
  data: {
    role: "user",
    content: "Updated content",
  },

  // Optional, timestamp in milliseconds. Helps prevents race conditions,
  // messages that arrive after a newer message are ignored
  updatedAt: Date.now(),
});
```

#### Liveblocks.deleteFeedMessage [#delete-rooms-roomId-feeds-feedId-messages-messageId]

Deletes a feed message. This is a wrapper around the
[Delete Feed Message API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-feeds-feedId-messages-messageId).

```ts
await liveblocks.deleteFeedMessage({
  roomId: "my-room-id",
  feedId: "my-feed-id",
  messageId: "my-message-id",
});
```

### Notifications

#### Liveblocks.getInboxNotifications [#get-users-userId-inboxNotifications]

Returns a list of a user’s inbox notifications. This is a wrapper around the
[Get Inbox Notifications API](/docs/api-reference/rest-api-endpoints#get-users-userId-inboxNotifications).
It also provides an unread query parameter to filter unread notifications.

```ts
const { data: inboxNotifications, nextCursor } =
  await liveblocks.getInboxNotifications({ userId: "steven@example.com" });

// [{ id: "in_3dH7sF3...", kind: "thread", ... }, { id: "in_3dH7sF3...", kind: "textMention", ... }, ...]
console.log(inboxNotifications);

// Filter unread notifications
const { data: unreadInboxNotifications, nextCursor } =
  await liveblocks.getInboxNotifications({
    userId: "steven@example.com",
    query: { unread: true },

    // Optional, filter for a specific organization
    organizationId: "acme-corp",
  });
```

##### Pagination

You can use `nextCursor` to paginate inbox notifications. In this example, when
`getNextPage` is called, the next page of inbox notifications is added to
`pages`.

```ts
import { InboxNotificationData } from "@liveblocks/node";

// An array of pages, each containing a list of retrieved inbox notifications
const pages: InboxNotificationData[][] = [];

// Holds the pagination cursor for the next set of inbox notifications
let startingAfter;

// Call to get the next page of inbox notifications
async function getNextPage() {
  const { data, nextCursor } = await liveblocks.getInboxNotifications({
    startingAfter,
  });
  pages.push(data);
  startingAfter = nextCursor;
}
```

If you’d like to iterate over all your inbox notifications, it’s most convenient
to use
[`liveblocks.iterInboxNotifications`](#iter-users-userId-inboxNotifications)
instead. This method automatically paginates your API requests.

#### Liveblocks.iterInboxNotifications [#iter-users-userId-inboxNotifications]

Returns a list of inbox notifications for the given user. Works similarly to
[`liveblocks.getInboxNotifications`](#get-users-userId-inboxNotifications), but
instead returns an asynchronous iterator, which helps you iterate over all the
inbox notifications, without having to manually paginate through the results.

```ts
const userId = "steven@example.com";

for await (const item of liveblocks.iterInboxNotifications({
  userId,
  // Optional, filter for a specific organization
  organizationId: "acme-corp",
})) {
  console.log(item.id); // in_3dH7sF3...
  console.log(item.kind); // "thread", "textMention", ...
}
```

#### Liveblocks.getInboxNotification [#get-users-userId-inboxNotifications-inboxNotificationId]

Returns a user’s inbox notification. This is a wrapper around the
[Get Inbox Notification API](/docs/api-reference/rest-api-endpoints#get-users-userId-inboxNotifications-inboxNotificationId).

```ts
const inboxNotification = await liveblocks.getInboxNotification({
  userId: "steven@example.com",
  inboxNotificationId: "in_3dH7sF3...",
});

// { id: "in_3dH7sF3...", kind: "thread", ... }
// or { id: "in_3dH7sF3...", kind: "textMention", ... }
// or { id: "in_3dH7sF3...", kind: "$yourKind", ... }
console.log(inboxNotification);
```

#### Liveblocks.triggerInboxNotification [#post-inbox-notifications-trigger]

Triggers a custom inbox notification. `kind` must start with a `$`, and
represents the type of notification. `activityData` is used to send custom data
with the notification, and properties can have `string`, `number`, or `boolean`
values. Notifications [can be batched](#Batching-custom-notifications). This is
a wrapper around the
[Trigger Inbox Notification API](/docs/api-reference/rest-api-endpoints#post-inbox-notifications-trigger).

```ts
await liveblocks.triggerInboxNotification({
  // The ID of the user that will receive the inbox notification
  userId: "steven@example.com",

  // The custom notification kind, must start with a $
  kind: "$fileUploaded",

  // Custom ID for this specific notification
  subjectId: "my-file",

  // Custom data related to the activity that you need to render the inbox notification
  activityData: {
    // Data can be a string, number, or boolean
    file: "https://example.com/my-file.zip",
    size: 256,
    success: true,
  },

  // Optional, define the room ID the notification was sent from
  roomId: "my-room-id",

  // Optional, trigger it for a specific organization
  organizationId: "acme-corp",
});
```

##### Typing custom notifications

To type custom notifications, edit the `ActivitiesData` type in your config
file.

```ts file="liveblocks.config.ts" highlight="4-10"
declare global {
  interface Liveblocks {
    // Custom activities data for custom notification kinds
    ActivitiesData: {
      // Example, a custom $alert kind
      $alert: {
        title: string;
        message: string;
      };
    };

    // Other kinds
    // ...
  }
}
```

##### Batching custom notifications

You can configure a custom notification kind to have batching enabled. When it’s
enabled, triggering an inbox notification activity for a specific `subjectId`,
will update the existing inbox notification instead of creating a new one.

To use this, you must first
[enable batching in the dashboard](/docs/products/notifications/concepts#Notification-batching).
Next, trigger a notification with the same `subjectId` as an existing
notification, and the result will be added to the `activityData` array.

```ts
const options = {
  userId: "steven@example.com",
  kind: "$fileUploaded",
  subjectId: "my-file",
};

await liveblocks.triggerInboxNotification({
  ...options,

  // +++
  activityData: {
    status: "processing",
  },
  // +++
});

await liveblocks.triggerInboxNotification({
  ...options,

  // +++
  activityData: {
    status: "complete",
  },
  // +++
});

const { data: inboxNotifications } = await liveblocks.getInboxNotifications({
  userId: "steven@example.com",
});

// {
//   id: "in_3dH7sF3...",
//   kind: "$fileUploaded",
// +++
//   activities: [
//     { status: "processing" },
//     { status: "complete" },
//   ],
// +++
//   ...
// }
console.log(inboxNotifications[0]);
```

An inbox notification can have up to 50 activities, if you exceed this number, a
new inbox notification will be created.

#### Liveblocks.deleteInboxNotification [#delete-users-userId-inboxNotifications-inboxNotificationId]

Deletes a user’s inbox notification. This is a wrapper around the
[Delete Inbox Notification API](/docs/api-reference/rest-api-endpoints#delete-users-userId-inboxNotifications-inboxNotificationId).

```ts
await liveblocks.deleteInboxNotification({
  userId: "steven@example.com",
  inboxNotificationId: "in_3dH7sF3...",
});
```

#### Liveblocks.deleteAllInboxNotifications [#delete-users-userId-inboxNotifications]

Deletes all the user’s inbox notifications. This is a wrapper around the
[Delete Inbox Notifications API](/docs/api-reference/rest-api-endpoints#delete-users-userId-inboxNotifications).

```ts
await liveblocks.deleteAllInboxNotifications({
  userId: "steven@example.com",
  // Optional, delete for a specific organization
  organizationId: "acme-corp",
});
```

#### Liveblocks.getNotificationSettings [#get-users-userId-notification-settings] [@badge=Beta]

Returns a user’s notification settings in the current project, in other words
which
[notification webhook events](/docs/api-reference/webhook-events#NotificationEvent)
will be sent for the user. Notification settings are project-based, which means
that this returns the user’s settings for every room. This a wrapper around the
[Get Notification Settings API](/docs/api-reference/rest-api-endpoints#get-users-userId-notification-settings).

```ts
const settings = await liveblocks.getNotificationSettings({
  userId: "guillaume@liveblocks.io",
});

// { email: { thread: true, ... }, slack: { thread: false, ... }, ... }
console.log(settings);
```

A user’s initial settings are set in the dashboard, and different kinds should
be enabled there. If no kind is enabled on the current channel, `null` will be
returned. For example, with the email channel:

```ts
const settings = await liveblocks.getNotificationSettings({
  userId: "guillaume@liveblocks.io",
});

// { email: null, ... }
console.log(settings);
```

#### Liveblocks.updateNotificationSettings [#post-users-userId-notification-settings] [@badge=Beta]

Updates a user’s notification settings, which affects which
[notification webhook events](/docs/api-reference/webhook-events#NotificationEvent)
will be sent for the user. Notification settings are project-based, which means
that this modifies the user’s settings in every room. Each notification `kind`
must first be enabled on your project’s notification dashboard page before
settings can be used. This a wrapper around the
[Update Notification Settings API](/docs/api-reference/rest-api-endpoints#post-users-userId-notification-settings).

```ts
const updatedSettings = await liveblocks.updateNotificationSettings({
  userId: "steven@example.com",
  data: {
    email: { thread: false },
    slack: { textMention: true },
  },
});

// { email: { thread: false, ... }, slack: { textMention: true, ... }, ... }
console.log(updatedSettings);
```

You can pass a partial object, or many settings at once.

```ts
// You only need to pass partials
await liveblocks.updateNotificationSettings({
  userId: "steven@example.com",
  email: { thread: true },
});

// Enabling a custom notification on the slack channel
await liveblocks.updateNotificationSettings({
  userId: "steven@example.com",
  slack: { $myCustomNotification: true },
});

// Setting complex settings
await liveblocks.updateNotificationSettings({
  userId: "steven@example.com",
  email: {
    thread: true,
    textMention: false,
    $newDocument: true,
  },
  slack: {
    thread: false,
    $fileUpload: false,
  },
  teams: {
    thread: true,
  },
});
```

#### Liveblocks.deleteNotificationSettings [#delete-users-userId-notification-settings] [@badge=Beta]

Deletes the user’s notification settings, resetting them to the default values.
The default values can be adjusted in a project’s notification dashboard page.
This a wrapper around the
[Delete Notification Settings API](/docs/api-reference/rest-api-endpoints#delete-users-userId-notification-settings).

```ts
await liveblocks.deleteNotificationSettings({
  userId: "adri@example.com",
});
```

### AI Copilots

#### Liveblocks.getAiCopilots [#get-ai-copilots]

Returns a paginated list of AI copilots. The copilots are returned sorted by
creation date, from newest to oldest. This is a wrapper around the
[Get AI Copilots API](/docs/api-reference/rest-api-endpoints#get-ai-copilots)
and returns the same response.

```ts
const { data: copilots, nextCursor } = await liveblocks.getAiCopilots();

// A list of AI copilots
// [{ type: "copilot", id: "co_abc123...", name: "My Copilot", ... }, ...]
console.log(copilots);

// A pagination cursor used for retrieving the next page of results with `startingAfter`
// "L3YyL3Jvb21z..."
console.log(nextCursor);
```

Pagination options are available to control the number of results returned.

```ts
const { data: copilots, nextCursor } = await liveblocks.getAiCopilots({
  // Optional, the amount of copilots to load, between 1 and 100, defaults to 20
  limit: 20,

  // Optional, cursor used for pagination, use `nextCursor` from the previous page's response
  startingAfter: "L3YyL3Jvb21z...",
});
```

#### Liveblocks.createAiCopilot [#create-ai-copilot]

Creates a new AI copilot with the given configuration. This is a wrapper around
the
[Create AI Copilot API](/docs/api-reference/rest-api-endpoints#create-ai-copilot)
and returns the same response.

```ts
const copilot = await liveblocks.createAiCopilot({
  name: "My AI Assistant",
  systemPrompt: "You are a helpful AI assistant for our team.",
  provider: "openai",
  providerModel: "gpt-4",
  providerApiKey: "sk-...", // Your OpenAI API key
});

// { type: "copilot", id: "co_abc123...", name: "My AI Assistant", ... }
console.log(copilot);
```

The method supports various configuration options for different AI providers.

```ts
const copilot = await liveblocks.createAiCopilot({
  // Required, the name of the copilot
  name: "Documentation Helper",

  // Optional, a description of what the copilot does
  description: "Helps users understand our documentation",

  // Required, the system prompt that defines the copilot's behavior
  systemPrompt:
    "You are an expert at helping users understand technical documentation.",

  // Optional, additional knowledge context for the copilot
  knowledgePrompt: "Use our company's style guide when providing examples.",

  // Optional, always retrieve knowledge sources on each query
  alwaysUseKnowledge: true,

  // Required, the AI provider to use
  provider: "openai",

  // Required for standard providers, the model to use
  providerModel: "gpt-4-turbo",

  // Required, your API key for the provider
  providerApiKey: "sk-...",

  // Optional, provider-specific options
  providerOptions: {
    openai: {
      reasoningEffort: "low",
      // Optional, restrict web search to specific domains for OpenAI
      webSearch: {
        allowedDomains: ["docs.liveblocks.io", "example.com"],
      },
    },
  },

  // Optional, model settings
  settings: {
    maxTokens: 1000,
    temperature: 0.7,
    topP: 0.9,
    frequencyPenalty: 0.1,
    presencePenalty: 0.1,
    stopSequences: ["END"],
    seed: 42,
    maxRetries: 3,
  },
});
```

For OpenAI-compatible providers, use a different configuration:

```ts
const copilot = await liveblocks.createAiCopilot({
  name: "Custom AI Helper",
  systemPrompt: "You are a helpful assistant.",
  provider: "openai-compatible",
  compatibleProviderName: "my-custom-provider",
  providerBaseUrl: "https://api.mycustomprovider.com/v1",
  providerApiKey: "your-api-key-here", // Your API key for the custom provider
  providerModel: "custom-provider-model",
});
```

You can also configure Anthropic or Google providers with provider-specific
options:

```ts
// Anthropic example
await liveblocks.createAiCopilot({
  name: "Anthropic Helper",
  systemPrompt: "You are a helpful assistant.",
  provider: "anthropic",
  providerModel: "claude-3-5-sonnet-latest",
  providerApiKey: "sk-...",
  providerOptions: {
    anthropic: {
      thinking: { type: "disabled" },
      webSearch: {
        allowedDomains: ["example.com"],
      },
    },
  },
});

// Google example
await liveblocks.createAiCopilot({
  name: "Gemini Helper",
  systemPrompt: "You are a helpful assistant.",
  provider: "google",
  providerModel: "gemini-2.5-pro",
  providerApiKey: "sk-...",
  providerOptions: {
    google: {
      thinkingConfig: { thinkingBudget: 2000 },
    },
  },
});
```

#### Liveblocks.getAiCopilot [#get-ai-copilot]

Returns an AI copilot by its ID. Throws an error if the copilot isn’t found.
This is a wrapper around the
[Get AI Copilot API](/docs/api-reference/rest-api-endpoints#get-ai-copilot) and
returns the same response.

```ts
const copilot = await liveblocks.getAiCopilot("co_abc123...");

// { type: "copilot", id: "co_abc123...", name: "My AI Assistant", ... }
console.log(copilot);
```

#### Liveblocks.updateAiCopilot [#update-ai-copilot]

Updates an existing AI copilot’s configuration. You only need to pass the
properties you want to update. Throws an error if the copilot isn’t found. This
is a wrapper around the
[Update AI Copilot API](/docs/api-reference/rest-api-endpoints#update-ai-copilot)
and returns the same response.

```ts
const updatedCopilot = await liveblocks.updateAiCopilot("co_abc123...", {
  name: "Updated AI Assistant",
  description: "Now with improved capabilities",
});

// { type: "copilot", id: "co_abc123...", name: "Updated AI Assistant", ... }
console.log(updatedCopilot);
```

You can update various aspects of the copilot:

```ts
const updatedCopilot = await liveblocks.updateAiCopilot("co_abc123...", {
  // Optional, update the name
  name: "Better AI Helper",

  // Optional, update the description
  description: "Enhanced with new features",

  // Optional, update the system prompt
  systemPrompt: "You are an even more helpful AI assistant.",

  // Optional, update the knowledge prompt
  knowledgePrompt: "Reference our latest guidelines.",

  // Optional, always retrieve knowledge sources on each query
  alwaysUseKnowledge: true,

  // Optional, update provider-specific options (replaces the entire nested options object)
  // Set to null to clear options
  providerOptions: null,

  // Optional, update model settings
  settings: {
    temperature: 0.5,
    maxTokens: 1500,
  },
});
```

You can also update provider options. When updating `providerOptions`, it fully
replaces the previous nested options (no deep merge):

```ts
// Update OpenAI provider options (replaces prior options)
await liveblocks.updateAiCopilot("co_abc123...", {
  provider: "openai",
  providerOptions: {
    openai: {
      reasoningEffort: "medium",
      webSearch: { allowedDomains: ["docs.liveblocks.io"] },
    },
  },
});

// Update Anthropic thinking/web search settings
await liveblocks.updateAiCopilot("co_abc123...", {
  provider: "anthropic",
  providerOptions: {
    anthropic: {
      thinking: { type: "enabled", budgetTokens: 2000 },
      webSearch: { maxUses: 2 },
    },
  },
});
```

Certain properties can be set to `null` to clear them from the copilot’s
configuration. This includes `description`, `knowledgePrompt`, `settings`, and
`providerOptions`.

```ts
const updatedCopilot = await liveblocks.updateAiCopilot("co_abc123...", {
  // Clear the description
  description: null,

  // Clear the knowledge prompt
  knowledgePrompt: null,

  // Clear all model settings
  settings: null,
});
```

The method returns a 422 response if the update doesn’t apply due to validation
failures. For example, if the existing copilot uses the "openai" provider and
you attempt to update the provider model to an incompatible value for the
provider, like "gemini-2.5-pro", you’ll receive a 422 response with an error
message explaining where the validation failed.

#### Liveblocks.deleteAiCopilot [#delete-ai-copilot]

Deletes an AI copilot by its ID. A deleted copilot is no longer accessible and
cannot be restored. Throws an error if the copilot isn’t found. This is a
wrapper around the
[Delete AI Copilot API](/docs/api-reference/rest-api-endpoints#delete-ai-copilot)
and returns no response.

```ts
await liveblocks.deleteAiCopilot("co_abc123...");
```

### Knowledge Sources

#### Liveblocks.createWebKnowledgeSource [#create-web-knowledge-source]

Creates a web knowledge source for an AI copilot. This allows the copilot to
access and learn from web content. This is a wrapper around the
[Create Web Knowledge Source API](/docs/api-reference/rest-api-endpoints#create-web-knowledge-source)
and returns the ID of the created knowledge source.

```ts
const { id } = await liveblocks.createWebKnowledgeSource({
  copilotId: "co_abc123...",
  url: "https://example.com/documentation",
  type: "individual_link",
});

// "ks_def456..."
console.log(id);
```

Different types of web knowledge sources are supported:

```ts
// Index a single web page
const singlePage = await liveblocks.createWebKnowledgeSource({
  copilotId: "co_abc123...",
  url: "https://example.com/important-page",
  type: "individual_link",
});

// Crawl an entire website
const crawledSite = await liveblocks.createWebKnowledgeSource({
  copilotId: "co_abc123...",
  url: "https://example.com",
  type: "crawl",
});

// Use a sitemap to index multiple pages
const sitemapSource = await liveblocks.createWebKnowledgeSource({
  copilotId: "co_abc123...",
  url: "https://example.com/sitemap.xml",
  type: "sitemap",
});
```

#### Liveblocks.createFileKnowledgeSource [#create-file-knowledge-source]

Creates a file knowledge source for an AI copilot by uploading a file. The
copilot can then reference the content of the file when responding. This is a
wrapper around the
[Create File Knowledge Source API](/docs/api-reference/rest-api-endpoints#create-file-knowledge-source)
and returns the ID of the created knowledge source.

**Note:** Currently only PDF files (`application/pdf`) and images (`image/*`)
are supported.

```ts
const { id } = await liveblocks.createFileKnowledgeSource({
  copilotId: "co_abc123...",
  file: pdfFile, // Must be a PDF or image file
});

// "ks_ghi789..."
console.log(id);
```

#### Liveblocks.getKnowledgeSources [#get-knowledge-sources]

Returns a paginated list of knowledge sources for a specific AI copilot. This is
a wrapper around the
[Get Knowledge Sources API](/docs/api-reference/rest-api-endpoints#get-knowledge-sources)
and returns the same response.

```ts
const { data: sources, nextCursor } = await liveblocks.getKnowledgeSources({
  copilotId: "co_abc123...",
});

// [{ type: "ai-knowledge-web-source", id: "ks_abc123...", ... }, ...]
console.log(sources);
```

Pagination options are available:

```ts
const { data: sources, nextCursor } = await liveblocks.getKnowledgeSources({
  copilotId: "co_abc123...",
  // Optional, the amount of knowledge sources to load, between 1 and 100, defaults to 20
  limit: 20,
  // Optional, cursor used for pagination
  startingAfter: "L3YyL3Jvb21z...",
});
```

#### Liveblocks.getKnowledgeSource [#get-knowledge-source]

Returns a specific knowledge source by its ID. Throws an error if the knowledge
source isn’t found. This is a wrapper around the
[Get Knowledge Source API](/docs/api-reference/rest-api-endpoints#get-knowledge-source)
and returns the same response.

```ts
const source = await liveblocks.getKnowledgeSource({
  copilotId: "co_abc123...",
  knowledgeSourceId: "ks_def456...",
});

// { type: "ai-knowledge-web-source", id: "ks_def456...", ... }
// or { type: "ai-knowledge-file-source", id: "ks_def456...", ... }
console.log(source);
```

#### Liveblocks.getFileKnowledgeSourceMarkdown [#get-file-knowledge-source-markdown]

Returns the content of a file knowledge source as Markdown. This allows you to
see what content the AI copilot has access to from uploaded files. Throws an
error if the knowledge source isn’t found. This is a wrapper around the
[Get File Knowledge Source Content API](/docs/api-reference/rest-api-endpoints#get-file-knowledge-source-content)
and returns the content as a string.

```ts
const content = await liveblocks.getFileKnowledgeSourceMarkdown({
  copilotId: "co_abc123...",
  knowledgeSourceId: "ks_def456...",
});

// "# Document Title\n\nThis is the content of the uploaded file..."
console.log(content);
```

#### Liveblocks.getWebKnowledgeSourceLinks [#get-web-knowledge-source-links]

Returns a paginated list of links that were indexed from a web knowledge source.
This is useful for understanding what content the AI copilot has access to from
web sources. This is a wrapper around the
[Get Web Knowledge Source Links API](/docs/api-reference/rest-api-endpoints#get-web-knowledge-source-links)
and returns the same response.

```ts
const { data: links, nextCursor } = await liveblocks.getWebKnowledgeSourceLinks(
  {
    copilotId: "co_abc123...",
    knowledgeSourceId: "ks_def456...",
  }
);

// [{ id: "link_123...", url: "https://example.com/page1", status: "ready", ... }, ...]
console.log(links);
```

Pagination options are available:

```ts
const { data: links, nextCursor } = await liveblocks.getWebKnowledgeSourceLinks(
  {
    copilotId: "co_abc123...",
    knowledgeSourceId: "ks_def456...",
    // Optional, the amount of links to load, between 1 and 100, defaults to 20
    limit: 20,
    // Optional, cursor used for pagination
    startingAfter: "L3YyL3Jvb21z...",
  }
);
```

#### Liveblocks.deleteWebKnowledgeSource [#delete-web-knowledge-source]

Deletes a web knowledge source from an AI copilot. The copilot will no longer
have access to the content from this source. Throws an error if the knowledge
source isn’t found. This is a wrapper around the
[Delete Web Knowledge Source API](/docs/api-reference/rest-api-endpoints#delete-web-knowledge-source)
and returns no response.

```ts
await liveblocks.deleteWebKnowledgeSource({
  copilotId: "co_abc123...",
  knowledgeSourceId: "ks_def456...",
});
```

#### Liveblocks.deleteFileKnowledgeSource [#delete-file-knowledge-source]

Deletes a file knowledge source from an AI copilot. The copilot will no longer
have access to the content from this file. Throws an error if the knowledge
source isn’t found. This is a wrapper around the
[Delete File Knowledge Source API](/docs/api-reference/rest-api-endpoints#delete-file-knowledge-source)
and returns no response.

```ts
await liveblocks.deleteFileKnowledgeSource({
  copilotId: "co_abc123...",
  knowledgeSourceId: "ks_def456...",
});
```

### Error handling [#error-handling]

Errors in our API methods, such as network failures, invalid arguments, or
server-side issues, are reported through the `LiveblocksError` class. This
custom error class extends the standard JavaScript
[`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
and includes a `status` property, which provides the HTTP status code for the
error, such as 404 for not found or 500 for server errors.

Example of handling errors in a typical API call:

```ts
try {
  const room = await liveblocks.getRoom("my-room-id");
  // Process room
} catch (error) {
  if (error instanceof LiveblocksError) {
    // Handle specific LiveblocksError cases
    console.error(`Error fetching room: ${error.status} - ${error.message}`);
    switch (
      error.status
      // Specific cases based on status codes
    ) {
    }
  } else {
    // Handle general errors
    console.error(`Unexpected error: ${error.message}`);
  }
}
```

## Utilities

### getMentionsFromCommentBody [#get-mentions-from-comment-body]

Returns an array of mentions from a `CommentBody` (found under `comment.body`).

```ts
import { getMentionsFromCommentBody } from "@liveblocks/node";

const mentions = getMentionsFromCommentBody(comment.body);
```

An optional second argument can be used to filter the returned mentions. By
default, if it’s not provided, all mentions are returned, including future
mention kinds (e.g. group mentions in the future).

```tsx
// All mentions (same as `getMentionsFromCommentBody(commentBody)`)
getMentionsFromCommentBody(commentBody);

// Only user mentions with an ID of "123"
getMentionsFromCommentBody(
  commentBody,
  (mention) => mention.kind === "user" && mention.id === "123"
);

// Only mentions with an ID which starts with "prefix:"
getMentionsFromCommentBody(commentBody, (mention) => (
  mention.id.startsWith("prefix:")
);
```

This is most commonly used in combination with the
[Comments API functions](/docs/api-reference/liveblocks-node#Comments), for
example [`getComment`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-comments-commentId).

```ts
import { Liveblocks, getMentionsFromCommentBody } from "@liveblocks/node";

// Create a node client
const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

// Retrieve a comment
const comment = await liveblocks.getComment({
  roomId: "my-room-id",
  threadId: "my-thread-id",
  commentId: "my-comment-id",
});

// Get the mentions inside the comment's body
const mentions = getMentionsFromCommentBody(comment.body);

// [{ kind: "user", id: "marc@example.com" }, { kind: "user", id: "vincent@example.com" }, ...]
console.log(mentions);
```

Here’s an example with a custom `CommentBody`.

```ts
import { CommentBody, getMentionsFromCommentBody } from "@liveblocks/node";

// Create a custom `CommentBody`
const commentBody: CommentBody = {
  version: 1,
  content: [
    {
      type: "paragraph",
      children: [
        { text: "Hello " },
        { type: "mention", id: "chris@example.com" },
      ],
    },
  ],
};

// Get the mentions inside the comment's body
const mentions = getMentionsFromCommentBody(commentBody);

// [{ kind: "user", id: "chris@example.com" }]
console.log(mentions);
```

<Banner title="Also available from @liveblocks/client">

If you’d like to use this on the client side, it’s also available from
[`@liveblocks/client`](/docs/api-reference/liveblocks-client#get-mentions-from-comment-body).

</Banner>

### stringifyCommentBody [#stringify-comment-body]

Used to convert a `CommentBody` (found under `comment.body`) into either a plain
string, Markdown, HTML, or a custom format.

```ts
import { stringifyCommentBody } from "@liveblocks/node";

const stringComment = await stringifyCommentBody(comment.body);
```

This is most commonly used in combination with the
[Comments API functions](/docs/api-reference/liveblocks-node#Comments), for
example [`getComment`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-comments-commentId).

```ts
import { Liveblocks, stringifyCommentBody } from "@liveblocks/node";

// Create a node client
const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

// Retrieve a comment
const comment = await liveblocks.getComment({
  roomId: "my-room-id",
  threadId: "my-thread-id",
  commentId: "my-comment-id",
});

// Convert CommentBody to plain string
const stringComment = await stringifyCommentBody(comment.body);

// "Hello marc@example.com from https://liveblocks.io"
console.log(stringComment);
```

A number of options are also available.

```ts
import { stringifyCommentBody } from "@liveblocks/client";

const stringComment = await stringifyCommentBody(comment.body, {
  // Optional, convert to specific format, "plain" (default) | "markdown" | "html"
  format: "markdown",

  // Optional, supply a separator to be used between paragraphs
  separator: `\n\n`,

  // Optional, override any elements in the CommentBody with a custom string
  elements: {
    // Optional, override the `paragraph` element
    paragraph: ({ element, children }) => `<p>${children}</p>`,

    // Optional, override the `text` element
    text: ({ element }) =>
      element.bold ? `<strong>${element.text}</strong>` : `${element.text}`,

    // Optional, override the `link` element
    link: ({ element, href }) =>
      `<a href="${href}" target="_blank">${element.url}</a>`,

    // Optional, override the `mention` element.
    // `user` and `group` are the optional data returned from `resolveUsers` and `resolveGroupsInfo`
    mention: ({ element, user, group }) =>
      `<a href="${user?.profileUrl ?? group?.settingsUrl ?? "#"}">${element.id}</a>`,
  },

  // Optional, get your user’s names and info from their ID to be displayed in mentions
  async resolveUsers({ userIds }) {
    const usersData = await __getUsersFromDB__(userIds);

    return usersData.map((userData) => ({
      // Name is inserted into the output instead of a user’s ID
      name: userData.name,

      // Custom formatting in `elements.mention` allows custom properties to be used
      profileUrl: userData.profileUrl,
    }));
  },

  // Optional, get your group’s names and info from their ID to be displayed in mentions
  async resolveGroupsInfo({ groupIds }) {
    const groupsData = await __getGroupsFromDB__(groupIds);

    return groupsData.map((groupData) => ({
      // Name is inserted into the output instead of a group’s ID
      name: groupData.name,

      // Custom formatting in `elements.mention` allows custom properties to be used
      settingsUrl: groupData.settingsUrl,
    }));
  },
});
```

<Banner title="Also available from @liveblocks/client">

If you’d like to use this on the client side, it’s also available from
[`@liveblocks/client`](/docs/api-reference/liveblocks-client#stringify-comment-body).

</Banner>

#### Formatting examples

Here are a number of different formatting examples derived from the same
`CommentBody`.

```ts
// "Hello marc@example.com from https://liveblocks.io"
await stringifyCommentBody(comment.body);

// "Hello @Marc from https://liveblocks.io"
await stringifyCommentBody(comment.body, {
  resolveUsers({ userIds }) {
    return [{ name: "Marc" }];
  },
});

// "**Hello** @Marc from [https://liveblocks.io](https://liveblocks.io)"
await stringifyCommentBody(comment.body, {
  format: "markdown",

  resolveUsers() {
    return [{ name: "Marc" }];
  },
});

// "<b>Hello</b> <span data-mention>@Marc</span> from
// <a href="https://liveblocks.io">https://liveblocks.io</a>"
await stringifyCommentBody(comment.body, {
  format: "html",

  resolveUsers() {
    return [{ name: "Marc" }];
  },
});

// "<b>Hello</b> <a href="https://example.com" data-id="marc@example.com">@Marc</a> from
// <a href="https://liveblocks.io">https://liveblocks.io</a>"
await stringifyCommentBody(comment.body, {
  format: "html",

  mention: ({ element, user }) =>
    `<a href="${user.profileUrl}" data-id="${element.id}">${user.name}</a>`,

  resolveUsers() {
    return [{ name: "Marc", profileUrl: "https://example.com" }];
  },
});
```

### markdownToCommentBody [#markdown-to-comment-body]

Converts a Markdown string into a `CommentBody` object that can be used to write
comments with [`createThread`](#post-rooms-roomId-threads),
[`createComment`](#post-rooms-roomId-threads-threadId-comments), or
[`editComment`](#post-rooms-roomId-threads-threadId-comments-commentId).

```ts
import { markdownToCommentBody } from "@liveblocks/node";

const body = markdownToCommentBody(`
@stacy Can you review this update?

You can add **bold**, _italics_, ~~strikethrough~~, and \`inline code\`.

Read more in [the docs](https://liveblocks.io/docs).
`);
```

This is a lossy conversion because `CommentBody` only supports paragraphs,
inline text formatting (bold, italic, strikethrough, code), links, and
`@mentions`. Unsupported features like headings, lists, tables, or blockquotes
are kept as plain text.

### WebhookHandler [#WebhookHandler]

<Banner title="Need help implementing webhooks?">

Read the [Webhooks guide](/docs/platform/webhooks) to learn how to use them
within your product, allowing you to react to Liveblocks events as they happen.

</Banner>

The `WebhookHandler` class is a helper to handle webhook requests from
Liveblocks.

It’s initialized with a webhook secret that you can find in your project’s
webhook page.

```js
const webhookHandler = new WebhookHandler(process.env.WEBHOOK_SECRET);
```

#### verifyRequest [#verifyRequest]

Verifies the request and returns the event. Note that `rawBody` takes the body
as a `string`.

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

Some frameworks parse request bodies into objects, which means using
`JSON.stringify` may be necessary.

```js highlight="3"
const event = webhookHandler.verifyRequest({
  headers: req.headers,
  rawBody: JSON.stringify(req.body),
});
```

##### Example using Next.js [#webhook-example]

```js
import { WebhookHandler } from "@liveblocks/node";

// Will fail if not properly initialized with a secret
// Obtained from the Webhooks section of your project dashboard
// https://liveblocks.io/dashboard
const webhookHandler = new WebhookHandler(process.env.WEBHOOK_SECRET);

export function POST(request) {
  try {
    const event = webhookHandler.verifyRequest({
      headers: req.headers,
      rawBody: JSON.stringify(req.body),
    });

    // Handle `WebhookEvent`

    if (event.type === "storageUpdated") {
      // Handle `StorageUpdatedEvent`
    } else if (event.type === "userEntered") {
      // Handle `UserEnteredEvent`
    } else if (event.type === "userLeft") {
      // Handle `UserLeftEvent`
    }
  } catch (error) {
    console.error(error);
    return new Response(error, { status: 400 });
  }
}
```

### isThreadNotificationEvent [#isThreadNotificationEvent]

Type guard to check if a received webhook event is a
[`ThreadNotificationEvent`](/docs/api-reference/webhook-events#Thread-notification)
send from Comments. Particularly helpful when creating
[thread notification emails](/docs/api-reference/liveblocks-emails#thread-notification-emails)
with webhooks.

```js
import { isThreadNotificationEvent } from "@liveblocks/node";

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

// +++
if (isThreadNotificationEvent(event)) {
  // Handle `ThreadNotificationEvent`
}
// +++
```

The check is made against the event type and event data kind.

### isTextMentionNotificationEvent [#isTextMentionNotificationEvent]

Type guard to check if a received webhook event is a
[`TextMentionNotificationEvent`](/docs/api-reference/webhook-events#TextMention-notification)
sent from Text Editor. Particularly helpful for identifying text mentions when
sending email notifications.

```js
import { isTextMentionNotificationEvent } from "@liveblocks/node";

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

// +++
if (isTextMentionNotificationEvent(event)) {
  // Handle `TextMentionNotificationEvent`
}
// +++
```

### isCustomNotificationEvent [#isCustomNotificationEvent]

Type guard to check if a received webhook event is a
[`CustomNotificationEvent`](/docs/api-reference/webhook-events#Custom-notification)
sent from
[`triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger).
Particularly helpful for identifying custom notifications when sending email
notifications.

```js
import { isCustomNotificationEvent } from "@liveblocks/node";

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

// +++
if (isCustomNotificationEvent(event)) {
  // Handle `CustomNotificationEvent`
}
// +++
```

The check is made against the event type and event data kind.

[`room.getothers`]: /docs/api-reference/liveblocks-client#Room.getOthers
[Permissions REST API]:
  /docs/api-reference/authentication#id-token-room-permissions

---

For an overview of all available documentation, see [/llms.txt](/llms.txt).
