---
meta:
  title: "Email notifications"
  parentTitle: "Comments"
  description: "Send notifications with webhooks"
---

Using Liveblocks webhooks, it’s possible to trigger your API endpoints when
certain events occur, such as a thread being created, or a comment being
modified. One use for these events is sending unread comment notifications, for
example via email or Slack.

<Figure>
  <Image
    src="/assets/comments/email-notification.png"
    alt="An email titled 'New notifications' showing two comments and a link to the thread"
    width={1500}
    height={1000}
  />
</Figure>

<Banner title="Step-by-step tutorial">

This page is an overview of creating email notifications for Comments. If you’d
prefer a full step-by-step tutorial, make sure to read
[how to send email notifications of unread comments](/docs/guides/how-to-send-email-notifications-of-unread-comments).

</Banner>

## Inbox notifications

Email notifications are built around the concept of inbox notifications, which
are different from “normal” notifications in the sense that they can group
multiple activities together and evolve over time, which makes more sense when
sending email notifications because it helps to avoid sending too many emails.
In the case of Comments, inbox notifications are grouped per thread, which means
that if there are 4 new comments in a thread you’re participating in, you will
have a single inbox notification for it, instead of 4 “normal” notifications.

Learn more about Notifications for Comments in the
[overview page](/docs/ready-made-features/comments/email-notifications).

## Sending email notifications with webhooks

Using [Liveblocks webhooks](/docs/platform/webhooks#Liveblocks-events) you can
listen to a range of events such as comments being deleted, or comment reactions
being added. On [your dashboard](https://liveblocks.io/dashboard) you can create
a webhook for a project, and select which events you’d like to listen to. You
only need to select the `"notification"` webhook event for our recommended
solution.

<Figure>
  <video width={1512} height={982} autoPlay loop muted playsInline>
    <source src="/assets/webhooks/edit-events.mp4" type="video/mp4" />
  </video>
</Figure>

The endpoint URL you pass will receive request with relevant data when the event
occurs. The webhook event built for creating these unread notification emails is
called `"notification"`, and by default is sent up to every 30 minutes to each
user, though this can be customized in the webhooks dashboard.

`"notification"` webhooks can be enabled and disabled on certain channels.
Channels are used to represent different places your users may receive
notifications, such as on `email`, `slack`, `teams`, and `webPush`.

<Figure>
  <video width={1512} height={982} autoPlay loop muted playsInline>
    <source src="/assets/webhooks/notification-settings.mp4" type="video/mp4" />
  </video>
</Figure>

Here’s an example of an event object that’s sent when a user receives a new (or
updated) inbox notification.

```ts
const event = {
  type: "notification",
  data: {
    channel: "email",
    kind: "thread",
    projectId: "my-project-id",
    roomId: "my-room-id",
    threadId: "th_d75sF3...",
    inboxNotificationId: "in_xt3p7ak...",
    userId: "my-user-id",
    createdAt: "2021-10-06T01:45:56.558Z",
  },
};
```

### Your endpoint

In your endpoint, you can use this `event` object to get information on unread
mentions and replies, and start rendering emails with
[HTML](/docs/api-reference/liveblocks-emails#prepare-thread-notification-email-as-html)
or
[React](/docs/api-reference/liveblocks-emails#prepare-thread-notification-email-as-react).

```tsx
// Get email data as React JSX
const emailData = await prepareThreadNotificationEmailAsReact(
  liveblocks,
  event
);

let email;
switch (emailData.type) {
  // The user has an unread mention
  case "unreadMention": {
    email = (
      // +++
      <div>
        <div>
          @{emailData.comment.author.id} at {emailData.comment.createdAt}
        </div>
        <div>{emailData.comment.body}</div>
      </div>
      // +++
    );
    break;
  }

  // The user has multiple unread replies
  case "unreadReplies": {
    email = (
      // +++
      <div>
        {emailData.comments.map((comment) => (
          <div key={comment.id}>
            <div>
              @{comment.author.id} at {comment.createdAt}
            </div>
            <div>{comment.body}</div>
          </div>
        ))}
      </div>
      // +++
    );
  }
}
```

You can customize this further, resolving user and room IDs into data, allowing
you to render names instead of IDs. You can also add custom React components to
the comment bodies, helpful for custom stying.

```tsx
// Get email data as React JSX
const emailData = await prepareThreadNotificationEmailAsReact(
  liveblocks,
  event,
  {
    // +++
    resolveUsers: async ({ userIds }) => {
      const usersData = await __getUsersFromDB__(userIds);

      return usersData.map((userData) => ({
        name: userData.name,
        avatar: userData.avatar.src,
      }));
    },
    // +++
    // ...
    components: {
      // `user` is the optional data returned from `resolveUsers`
      // +++
      Mention: ({ element, user }) => (
        <span style={{ color: "red" }}>@{user.name}</span>
      ),
      // +++

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

You can then send an email containing the comment to the owner of `userId`
received in `event`.

```ts
const emailAddress = __getUserEmail__(userId);

// Send email to the user that received the inbox notification
__sendEmail__({
  from: "hello@my-company.com",
  to: emailAddress,
  title: "Unread comment",
  react: `
    <h1>Unread comment</h1>
    ${commentHtml}
  `,
});
```

Here’s an example with every step linked together, along with the code necessary
to verify a webhook request is valid, and using [Resend](https://resend.com) to
send an email.

```ts title="Full example" isCollapsable isCollapsed
import {
  Liveblocks,
  WebhookHandler,
  isThreadNotificationEvent,
} from "@liveblocks/node";
import { prepareThreadNotificationEmailAsReact } from "@liveblocks/emails";
import { Resend } from "resend";

// Create Resend client (add your API key)
const resend = new Resend("re_123456789");

// Add your webhook secret key from a project's webhooks dashboard
const WEBHOOK_SECRET = "YOUR_WEBHOOK_SECRET_KEY";
const webhookHandler = new WebhookHandler(WEBHOOK_SECRET);

// Add your secret key from a project's API keys dashboard
const API_SECRET = "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx";
const liveblocks = new Liveblocks({ secret: API_SECRET });

export async function POST(request: Request) {
  const body = await request.json();
  const headers = request.headers;

  // Verify if this is a real webhook request
  let event;
  try {
    event = webhookHandler.verifyRequest({
      headers: headers,
      rawBody: JSON.stringify(body),
    });
  } catch (err) {
    console.error(err);
    return new Response("Could not verify webhook call", { status: 400 });
  }

  // When an inbox notification has been created
  if (isThreadNotificationEvent(event)) {
    // Check if user has access to room
    if (!__hasRoomAccess__(event.userId, event.roomId)) {
      return new Response(null, { status: 200 });
    }

    // The user to send the email to
    const emailAddress = __getEmailAddressFromDB__(event.userId);

    let emailData;

    try {
      emailData = await prepareThreadNotificationEmailAsReact(
        liveblocks,
        event,
        {
          resolveUsers: async ({ userIds }) => {
            const usersData = await __getUsersFromDB__(userIds);

            return usersData.map((userData) => ({
              name: userData.name, // "Steven"
              avatar: userData.avatar.src, // "https://example.com/steven.jpg"
            }));
          },
          resolveRoomInfo: async ({ roomId }) => {
            const roomData = await __getRoomFromDB__(roomId);

            return {
              name: roomData.name, // "Untitled document"
              url: roomData.url, //`https://example.com/my-room-id`
            };
          },
          components: {
            Paragraph: ({ children }) => (
              <p style={{ margin: "12px 0" }}>{children}</p>
            ),

            // `user` is the optional data returned from `resolveUsers`
            Mention: ({ element, user }) => (
              <span style={{ color: "red" }}>@{user?.name ?? element.id}</span>
            ),

            // If the link is rich-text render it, otherwise use the URL
            Link: ({ element, href }) => (
              <a href={href} style={{ textDecoration: "underline" }}>
                {element?.text ?? href}
              </a>
            ),
          },
        }
      );
    } catch (err) {
      console.log(err);
      return new Response("Could not fetch thread notification data", {
        status: 500,
      });
    }

    // All comments have already been read
    if (!emailData) {
      return new Response(null, { status: 200 });
    }

    let email;
    switch (emailData.type) {
      case "unreadMention": {
        email = (
          <div>
            <div>
              @{emailData.comment.author.id} at {emailData.comment.createdAt}
            </div>
            <div>{emailData.comment.body}</div>
          </div>
        );
        break;
      }

      case "unreadReplies": {
        email = (
          <div>
            {emailData.comments.map((comment) => (
              <div key={comment.id}>
                <div>
                  @{comment.author.id} at {comment.createdAt}
                </div>
                <div>{comment.body}</div>
              </div>
            ))}
          </div>
        );
        break;
      }
    }

    // Send email to the user's email address
    try {
      const data = await resend.emails.send({
        from: "My company <hello@my-company.com>",
        to: emailAddress,
        subject: "New comment",
        react: email,
      });
    } catch (err) {
      console.error(err);
    }
  }

  return new Response(null, { status: 200 });
}
```

If you’re planning on building this, we recommend learning more in our
[how to send email notifications](/docs/guides/how-to-send-email-notifications-of-unread-comments)
guide, as it’s possible to create much more complex emails than this simple
example.

### Permissions

When you receive a `notification` webhook event, it's essential to verify if the
user has access to the room before sending an email. Liveblocks lacks the
necessary information to determine if a user has access to a room. For instance,
we create an inbox notification when a user is mentioned in a comment. In this
user's client context, we can determine if they have access to the notification
thanks to the token generated for that user. However, when we send a
`notification` webhook event, we lack this information.

#### Access token authentication

If you are using access tokens, this will always be true, Liveblocks will never
have the information.

#### ID token authentication

If you are using ID tokens, Liveblocks already possesses certain information
about the permissions you have configured for each room, specifying which users
and groups have access. However, what we currently lack is the relationship
between a user and a group. At present, you need to verify user access before
sending an email. We do, however, plan to include full permissions info in
Liveblocks in our future updates. If you're interested in learning more about
this feature, please feel free to reach out to us.

## Webhook events

There are more webhook events than just the `NotificationEvent` event used
above—a number related to Comments are available to use.

- [`CommentCreatedEvent`](/docs/platform/webhooks#CommentCreatedEvent)
- [`CommentEditedEvent`](/docs/platform/webhooks#CommentEditedEvent)
- [`CommentDeletedEvent`](/docs/platform/webhooks#CommentDeletedEvent)
- [`CommentReactionAddedEvent`](/docs/platform/webhooks#CommentReactionAddedEvent)
- [`CommentReactionRemovedEvent`](/docs/platform/webhooks#CommentReactionRemovedEvent)
- [`CommentMetadataUpdatedEvent`](/docs/platform/webhooks#CommentMetadataUpdatedEvent)
- [`ThreadCreatedEvent`](/docs/platform/webhooks#ThreadCreatedEvent)
- [`ThreadMetadataUpdatedEvent`](/docs/platform/webhooks#ThreadMetadataUpdatedEvent)
- [`ThreadMarkedAsResolvedEvent`](/docs/platform/webhooks#ThreadMarkedAsResolvedEvent)
- [`ThreadMarkedAsUnresolvedEvent`](/docs/platform/webhooks#ThreadMarkedAsUnresolvedEvent)

There are also more events, for example you can trigger events when users enter
or leave rooms. We recommend reading our guide on
[testing webhooks locally](/docs/guides/how-to-test-webhooks-on-localhost) to
get started.

## Retrieving and modifying Comments data

Here’s every Comments-related
[`@liveblocks/node`](/docs/api-reference/liveblocks-node) function. Each also
has a corresponding REST API, you can find more info by following the links.

- [`getThreads`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads)
- [`createThread`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads)
- [`getThread`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId)
- [`getThreadParticipants`](/docs/api-reference/liveblocks-node#get-thread-participants)
- [`editThreadMetadata`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-metadata)
- [`createComment`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments)
- [`getComment`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-comments-commentId)
- [`editComment`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId)
- [`editCommentMetadata`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-metadata)
- [`deleteComment`](/docs/api-reference/liveblocks-node#delete-rooms-roomId-threads-threadId-comments-commentId)
- [`addCommentReaction`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction)
- [`removeCommentReaction`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-remove-reaction)
- [`getInboxNotification`](/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications-inboxNotificationId)
- [`getRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#get-rooms-roomId-users-userId-subscription-settings)
- [`updateRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#post-rooms-roomId-users-userId-subscription-settings)
- [`deleteRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#delete-rooms-roomId-users-userId-subscription-settings)

---

For an overview of all available documentation, see [/llms.txt](/llms.txt).
