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
- When a reply shows up: the moment it's written or, if it went to the queue, when it's approved.
- The person who gets it is the one being answered. On a reply to another reply, the author of that reply gets it, not the author of the main comment.
- Nobody is notified of a reply to themselves.
- If the reply is deleted, the notification disappears from the lists and the count.
Types
type | When |
|---|---|
reply | Someone replied to the person's comment. |
mention | Someone tagged the person with @username. Whoever was already notified by the reply doesn't get two. |
follow | A 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
| Way | What for | You do |
|---|---|---|
| The widget's bell | Show them inside the comment section | Nothing. It's built in. |
| API with the person's login | A bell at the top of your site or in your app | Call it from the browser with the same token as the widget |
| API from your server | Show them or mark them as read from your backend | Call it with the secret key, naming the user |
| Webhook and feed | Copy them into the notification system your site already has | Receive 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.
/v1/notifications| Parameter | |
|---|---|
unread=1 | Only unread ones. |
limit | How many, from 1 to 100. Default: 30. |
before | For paging: the next value of the previous answer. |
{ "notifications": [ ... ], "unread": 3, "next": 290 } // next: null when there's no more
/v1/notifications/countJust the number of unread ones, light enough to check every so often: {"unread": 3}.
/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.
/v1/site/users/:userId/notificationsThe same parameters and the same answer as /v1/notifications.
/v1/site/users/:userId/notifications/count/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:
- Webhook to arrive right away
The
notification.createdevent arrives the instant the reply shows up, with the whole notification innotification. - 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.
- Store by id
The
idis unique: if the same notification arrives through the webhook and the feed, save it only once.
/v1/site/notifications?since=290&limit=100All 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.