Skip to content

Notifications

When someone replies to a comment, its author gets a notification. Qorum stores these notifications, and your site can show them in the widget, in its own notification system, or both.

When a notification is born

Types

typeWhen
replySomeone replied to the person's comment.
mentionSomeone tagged the person with @username. Whoever was already notified by the reply doesn't get two.
followA new comment on a conversation the person follows. While unread, notices for the same conversation become one, with count adding up ("3 new comments"). The webhook only goes out for the first of each batch.

Nobody gets notices from someone they blocked.

How it arrives

The same format everywhere: in the API, in management, in the feed and in the webhook.

{
  "id": 318,
  "type": "reply",
  "userId": 77,                       // who gets it (id on your site, or "q:N" for a Qorum account)
  "read": false,
  "createdAt": "2026-11-08T21:00:00Z",
  "actorId": 123,                     // who replied
  "actor": { "id": 123, "username": "ana_lima", "displayName": "Ana Lima", "avatar": "https://..." },
  "comment": { "id": 82, "html": "I agree, and...", "preview": "I agree, and..." },   // the reply
  "inReplyTo": { "id": 81, "preview": "I think the best way is..." },             // the recipient's comment
  "thread": { "ref": "post-42", "title": "My post", "url": "https://mysite.com/post-42" },
  "link": "https://mysite.com/post-42#qorum-82",    // opens the page right at the reply
  "commentId": 82, "preview": "I agree, and..."     // shortcuts (same as comment.id and comment.preview)
}

With this you can build the full sentence: "Ana Lima replied to your comment 'I think the best way is...' on My post", with a direct link to the reply. link only exists if the conversation has a url (the widget sends it by itself).

Four ways to use them

WayWhat forYou do
The widget's bellShow them inside the comment sectionNothing. It's built in.
API with the person's loginA bell at the top of your site or in your appCall it from the browser with the same token as the widget
API from your serverShow them or mark them as read from your backendCall it with the secret key, naming the user
Webhook and feedCopy them into the notification system your site already hasReceive the notice right away and, to be safe, pull the feed

With the person's login

The same routes the widget's bell uses. Send the public key and the person's token, just like the public API.

GET/v1/notifications
Parameter
unread=1Only unread ones.
limitHow many, from 1 to 100. Default: 30.
beforeFor paging: the next value of the previous answer.
{ "notifications": [ ... ], "unread": 3, "next": 290 }   // next: null when there's no more
GET/v1/notifications/count

Just the number of unread ones, light enough to check every so often: {"unread": 3}.

POST/v1/notifications/read

{"ids": [318, 317]} marks those; an empty body marks all. Returns how many were marked in marked.

Example: a bell at the top of your site

// uses the same login token from your site that you pass to the widget
async function qorum(path, options = {}) {
  const r = await fetch('https://useqorum.com' + path, {
    ...options,
    headers: { 'X-Qorum-Key': 'pk_YOUR_PUBLIC_KEY', Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
  });
  return r.json();
}

async function updateBell() {
  const { unread } = await qorum('/v1/notifications/count');
  document.querySelector('#bell .number').textContent = unread || '';
}

async function openBell() {
  const { notifications } = await qorum('/v1/notifications?limit=20');
  list.innerHTML = '';
  for (const n of notifications) {
    const item = document.createElement('a');
    item.href = n.link || '#';
    item.className = n.read ? '' : 'new';
    item.textContent = `${n.actor.displayName || n.actor.username} replied: "${n.preview}"`;
    list.appendChild(item);
  }
  await qorum('/v1/notifications/read', { method: 'POST', body: '{}' });
  updateBell();
}

setInterval(updateBell, 60000);   // every minute
updateBell();

This request comes from the browser, so the page must be on a domain allowed for the site, like the widget.

From your server

With the public and the secret key, like the management API. You say whose notifications they are in the address.

GET/v1/site/users/:userId/notifications

The same parameters and the same answer as /v1/notifications.

GET/v1/site/users/:userId/notifications/count
POST/v1/site/users/:userId/notifications/read
// the person read the notifications on your site: mark them in Qorum too, so the widget's bell agrees
await management('POST', '/users/77/notifications/read', { ids: [318, 317] });

:userId is the person's id on your site, the same sub as in the token. For Qorum accounts, use q:N.

Copy them into your site's notification system

If your site already has notifications (a bell, e-mail, mobile push), copy Qorum's into it. The safest way uses both things together:

  1. Webhook to arrive right away

    The notification.created event arrives the instant the reply shows up, with the whole notification in notification.

  2. Feed so you never lose one

    Every so often (or when your server starts), ask the feed for everything after the last notification you stored. It covers whatever was lost if your server was down.

  3. Store by id

    The id is unique: if the same notification arrives through the webhook and the feed, save it only once.

GET/v1/site/notifications?since=290&limit=100

All the site's notifications after id since, oldest first (up to 500 per request). userId says who gets each one.

{ "notifications": [ { "id": 291, "userId": 77, ... }, { "id": 292, "userId": 15, ... } ],
  "last": 292,      // store it and send it in since on the next request
  "more": false }   // true: there's more, ask again

Complete example (Node)

// 1) right away, through the webhook (see how to check the signature in /docs/webhooks)
async function handle(notice) {
  if (notice.event === 'notification.created') await store(notice.notification);
}

// 2) to be safe, every 5 minutes, through the feed
async function sync() {
  let since = await lastStoredId();              // 0 the first time
  for (;;) {
    const r = await management('GET', `/notifications?since=${since}&limit=500`);
    for (const n of r.notifications) await store(n);
    since = r.last;
    if (!r.more) break;
  }
}
setInterval(sync, 5 * 60 * 1000);

// saves once per id and delivers it to your site's system
async function store(n) {
  const isNew = await db.query(
    'INSERT INTO qorum_notifications (id, user_id, data) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING RETURNING id',
    [n.id, String(n.userId), n]);
  if (!isNew.rowCount || String(n.userId).startsWith('q:')) return;   // duplicate, or a Qorum account (not in your database)
  await notifyUser(n.userId, {
    title: `${n.actor.displayName || n.actor.username} replied to you`,
    text: n.preview,
    link: n.link,
  });
}

The answered comment

inReplyTo carries the comment of the person who got the reply, with a snippet of the text. If that comment was deleted, inReplyTo comes as null, but the notification is still valid as long as the reply exists.

The comment.created webhook also carries replyToCommentId and notifyUserId. If all you want is who to notify, that's enough. notification.created is more practical because it comes ready to show.