Management API
Everything the dashboard does, for your server. Use it to moderate from inside your site's admin panel, sync user cards, grant perks and follow the numbers.
Access
Every request carries both of the site's keys:
X-Qorum-Key: pk_YOUR_PUBLIC_KEY X-Qorum-Secret: sk_YOUR_SECRET_KEY
These calls don't work from a browser and must never be made from one. Whoever has the secret key has the power of the site owner.
A wrong secret key returns 401 "Invalid secret key". That happens right after rotating the keys in the dashboard, until you update your server.
// Node 18+: a helper for the calls below
async function management(method, path, body) {
const r = await fetch('https://useqorum.com/v1/site' + path, {
method,
headers: {
'X-Qorum-Key': process.env.QORUM_PUBLIC,
'X-Qorum-Secret': process.env.QORUM_SECRET,
'X-Qorum-Lang': 'en',
...(body ? { 'Content-Type': 'application/json' } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await r.json();
if (!r.ok) throw new Error(data.error);
return data;
}
Comments
/v1/site/commentsAll of the site's comments, newest first, 25 per page (or whatever you ask for in limit). Filters combine.
| Parameter | |
|---|---|
status | approved (default), pending (queue), deleted or all. |
q | Search in the text. |
ref | Only one conversation. |
refs | Several conversations, comma separated (up to 500). E.g. refs=post-1,post-2. |
refPrefix | Every conversation whose ref starts like this. E.g. refPrefix=product: takes product:12, product:80... You can split your site into sections just by how you build the ref. |
user | Only one person (the id on your site, or q:N). |
page | Page. |
limit | Items per page, from 1 to 100 (default 25). |
{ "comments": [ { "comment": { ...comment... }, "status": "pending", "deleted": false, "openReports": 0,
"canRestore": false,
"thread": { "ref": "post-42", "title": "My post", "url": "https://..." } } ],
"total": 4, "page": 1, "totalPages": 1 }
canRestore is true on deleted comments that can still come back. Those deleted along with the person's account (see Erasing someone's data) have no text left and can't come back.
/v1/site/comments/:id/approveApproves a comment from the queue. Right away it shows up, counts in the total, notifies whoever was answered and fires the comment.created webhook.
/v1/site/comments/:idDeletes any comment. For a comment in the queue, that means rejecting it. Deleting a main comment takes its replies with it.
/v1/site/comments/:id/restoreUndoes a delete. The comment comes back and, if it's a main comment, so do the replies that went with it. Replies that had been deleted before, one by one, stay deleted. The counters are rebuilt and your server gets the comment.restored webhook.
// answer
{ "message": "Comentário restaurado", "restoredReplies": 2 }
- To restore a reply whose main comment is also deleted, restore the main one first (the API answers
409explaining it). - A comment deleted along with the person's account doesn't come back (
409).
/v1/site/comments/:id/historyThe previous versions of an edited comment, newest first: {"comment": {...}, "edits": [{"content", "html", "editorId", "createdAt"}]}. Edits made before Nov 9, 2026 have no saved version.
/v1/site/comments/:id/pin and DELETEPins for {"hours": N} (default 24, max 8760) and unpins.
Conversations
/v1/site/threads?q=post&page=1The site's conversations, most recently active first. q searches the ref and the title.
{ "threads": [ { "ref": "post-42", "title": "My post", "url": "https://...", "locked": false,
"comments": 37, "rootComments": 21, "updatedAt": "...", "createdAt": "..." } ],
"total": 12, "page": 1, "totalPages": 1 }
/v1/site/threads/lock{"ref": "post-42", "locked": true} locks; false unlocks.
Reports and bans
/v1/site/reports?status=pending&page=1status: pending (default), reviewed, dismissed or all. It takes the same ref, refs, refPrefix and limit (1 to 100, default 20) as the comment list.
{ "reports": [ { "id": 5, "commentId": 81, "reason": "Spam or advertising", "status": "pending", "createdAt": "...",
"content": "reported text", "commentDeleted": false, "threadRef": "post-42", "threadUrl": "https://...",
"author": { ...card... }, "reporter": { ...card... } } ],
"total": 1, "page": 1, "totalPages": 1 }
/v1/site/reports/:id{"status": "reviewed", "deleteComment": true} deletes the comment; {"status": "dismissed"} dismisses it. Both resolve every open report of the same comment.
/v1/site/bans POST/v1/site/bans DELETE/v1/site/bans/:userId// ban for 7 days
await management('POST', '/bans', { userId: '123', reason: 'Repeated spam', hours: 168 });
// forever
await management('POST', '/bans', { userId: '123', reason: '...', permanent: true });
Users
/v1/site/users/:userId/cardUpdates someone's name and card right away (changed their photo, got a badge). Without it, the card only changes on the next login. Comments on screen can take up to 30 seconds to show the new card.
await management('PUT', '/users/123/card', {
name: 'Ana Lima',
card: { username: 'ana_lima', avatar: 'https://.../new-photo.jpg', rank: { name: 'Subscriber', nameColor: '#7c3aed' } }
});
/v1/site/users/cardsThe same, for many people at once: up to 1000 per request. The body has the same format as the data API answer, so you can reuse the code. Use it in a migration (so the name and photo of people who commented in the old system show up from the first visit) or after a change that hits a lot of people (a new badge for every subscriber).
await management('PUT', '/users/cards', {
users: {
'123': { name: 'Ana Lima', card: { username: 'ana_lima', avatar: 'https://.../ana.jpg' } },
'456': { name: 'Bruno', card: { username: 'bruno' } }
}
});
// answer: { "saved": 2, "ignored": 0 } (ignored = invalid id)
/v1/site/users/:userId/entitlements GETGrants perks. Today there's fixar_comentario (the person pins their own comment for 24 hours, spending one unit). The quantity is added to what the person already has; -1 is unlimited; hours sets until when it's valid.
// a subscriber got 3 uses valid for 30 days
await management('POST', '/users/123/entitlements', { item: 'fixar_comentario', quantity: 3, hours: 720 });
/v1/site/users/:userIdThe person deleted their account on your site: call this along with it. Qorum erases, all at once:
- the text and images of all their comments (they leave the screen, and the text isn't kept);
- the replies under their main comments;
- the likes and dislikes they gave, the reports they made, their notifications and the ones about them;
- the open reports against their comments (they leave the queue as reviewed);
- the card (name and photo), perks and ban.
The conversation and comment counters are rebuilt. It can't be undone.
await management('DELETE', '/users/123');
// answer: { "deletedComments": 14, "deletedReplies": 3, "removedVotes": 52 }
Notifications
Listing, counting and marking a user's notifications as read, plus the feed to copy everything into your system: see Notifications.
Numbers
/v1/site/stats{ "comments": 1284, "threads": 96, "last7Days": 311, "last30Days": 1102, "activeAuthors30Days": 240,
"pending": 4, "openReports": 1, "activeBans": 2, "webhookQueue": 0, "webhookFailed7Days": 0,
"updatedAt": "2026-11-09T14:00:00Z" }
The comment totals are kept for up to 1 minute (updatedAt says when they were counted); ?fresh=1 counts again. Pending, reports and webhooks are always live.
/v1/site/usage?days=30Meters per day, from 1 to 365 days. Today includes what has happened so far.
{ "days": [ { "day": "2026-11-08", "reads": 1840, "writes": 96, "comments": 41, "logins": 3,
"management": 12, "webhook_ok": 44, "webhook_err": 0, "denied": 2 } ],
"totals": { "reads": 1840, ... } }
| Meter | Counts |
|---|---|
reads | Lists, counts, config, single comment, "me" and notifications. |
writes | Commenting, replying, voting, editing, deleting, reporting, pinning, uploading images and moderating from the widget. |
comments | Comments and replies sent, including those that went to the queue. |
logins | Log ins and sign ups with a Qorum account. |
management | Calls from the dashboard and from this API. |
webhook_ok, webhook_err | Delivered notices and failed attempts. |
denied | Refused requests: domain not on the list, expired token, wrong secret key. |
Settings
/v1/site/settings PUTOn PUT, send only what you want to change. Inside config, also only the fields that change.
await management('PUT', '/settings', {
allowedOrigins: ['https://mysite.com', 'https://www.mysite.com'],
webhookUrl: 'https://mysite.com/qorum/webhook',
config: { template: 'cartoes', preModeracao: 'links', palavrasBloqueadas: ['casino', 'betting'] }
});
| Config field | Values |
|---|---|
template | classico, cartoes, bolhas, compacto, minimalista |
tema | auto, claro (light), escuro (dark) |
cor1, cor2 | Colors in #rrggbb format |
raio | Corners, from 0 to 20 |
ordem | recent, best, hot, oldest, likes_desc |
mostrarNaoCurti | true or false (dislike button) |
loginSite, loginQorum | Which logins are valid. If both end up false, the Qorum account is turned on. |
urlLogin | Where the Log in button goes when only your site's login is valid |
preModeracao | nao (off), links (link or image), tudo (everything) |
permitirImagens | true or false |
palavrasBloqueadas | List of texts (blocked words) |
reacoes, permitirVideos, permitirGifs | true or false (default: true) |
apiBusca | Address of your site's search API (links with #) |
Values outside what's allowed don't cause an error: they go back to the default. PUT returns the configuration as it ended up, so check the answer.
Keys, webhook and diagnostics
/v1/site/keys POST/v1/site/keys/rotateShows and rotates the keys. Rotating creates a new secret key and a new webhook secret, and the old ones stop working right away.
/v1/site/webhook/testSends a ping event right now. Returns {"ok": true, "status": 200, "ms": 84}, or the error.
/v1/site/diagnostics/tokenExplains what's wrong with a login token: {"token": "eyJ..."}.
{ "ok": false,
"problems": [ "aud precisa ser a chave PÚBLICA deste site: pk_...", "Token vencido em 08/11/2026 21:00" ],
"claims": { "sub": "123", "aud": "pk_wrong", ... } }
/v1/site/moderators POST DELETE/:accountIdThe site's moderators. To add one, {"username": "their_username"} of a Qorum account that already exists.
Bringing comments from another system
To migrate the comments your site already has. You can run it as many times as you want: each comment is identified by source and legacyId (its id in the old system), and what's already in is skipped.
/v1/site/import{
"source": "old_comments", // name of the original system (up to 40 characters)
"comments": [
{ "legacyId": 120, "ref": "post-42", "title": "My post", "url": "https://...",
"userId": "7", "content": "First!", "isEdited": false,
"createdAt": "2025-03-01T12:00:00Z", "updatedAt": "2025-03-01T12:00:00Z",
"likes": ["3", "9"], "dislikes": ["4"],
"reports": [ { "userId": "5", "reason": "Spam", "status": "pending", "createdAt": "..." } ] },
{ "legacyId": 121, "ref": "post-42", "parentLegacyId": 120, "replyToUserId": "7",
"userId": "3", "content": "Agreed", "createdAt": "2025-03-01T12:05:00Z" }
]
}
// answer: { "imported": 2, "skipped": 0, "orphans": 0, "invalid": 0 }
- Up to 1000 comments per request. Send main comments before replies: a reply looks for its parent by
parentLegacyId, in the same request or in earlier ones. - A reply to a reply sticks to the main comment, like everywhere else in Qorum, and the
@points to whoever was answered. orphansare replies whose parent didn't come (for example, it was deleted in the old system): they're left out.- Original dates are kept. Importing creates no notifications or webhooks.
- The counters (conversation total, replies, likes) are recalculated on every request.
- Names and photos of whoever commented: send all the authors' cards with
PUT /v1/site/users/cards(before or after importing). Without it, they come from the data API as pages are opened, or from each person's next login; until then "User" shows up. - Active bans: send them with
POST /v1/site/bans.
Example: an approval queue in your admin
Two routes on your server, using the helper from the top of the page. Your admin screen lists the pending ones and calls approve or reject:
app.get('/admin/comments/pending', adminOnly, async (req, res) => {
const { comments } = await management('GET', '/comments?status=pending');
res.json(comments.map(c => ({
id: c.comment.id,
author: c.comment.user.displayName || c.comment.user.username,
html: c.comment.html,
page: c.thread.url,
})));
});
app.post('/admin/comments/:id/:action', adminOnly, async (req, res) => {
const id = Number(req.params.id);
if (req.params.action === 'approve') await management('POST', `/comments/${id}/approve`);
else await management('DELETE', `/comments/${id}`);
res.sendStatus(204);
});