Skip to content

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
Server only.

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

GET/v1/site/comments

All of the site's comments, newest first, 25 per page (or whatever you ask for in limit). Filters combine.

Parameter
statusapproved (default), pending (queue), deleted or all.
qSearch in the text.
refOnly one conversation.
refsSeveral conversations, comma separated (up to 500). E.g. refs=post-1,post-2.
refPrefixEvery 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.
userOnly one person (the id on your site, or q:N).
pagePage.
limitItems 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.

POST/v1/site/comments/:id/approve

Approves a comment from the queue. Right away it shows up, counts in the total, notifies whoever was answered and fires the comment.created webhook.

DELETE/v1/site/comments/:id

Deletes any comment. For a comment in the queue, that means rejecting it. Deleting a main comment takes its replies with it.

POST/v1/site/comments/:id/restore

Undoes 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 }
GET/v1/site/comments/:id/history

The previous versions of an edited comment, newest first: {"comment": {...}, "edits": [{"content", "html", "editorId", "createdAt"}]}. Edits made before Nov 9, 2026 have no saved version.

POST/v1/site/comments/:id/pin and DELETE

Pins for {"hours": N} (default 24, max 8760) and unpins.

Conversations

GET/v1/site/threads?q=post&page=1

The 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 }
PUT/v1/site/threads/lock

{"ref": "post-42", "locked": true} locks; false unlocks.

Reports and bans

GET/v1/site/reports?status=pending&page=1

status: 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 }
PUT/v1/site/reports/:id

{"status": "reviewed", "deleteComment": true} deletes the comment; {"status": "dismissed"} dismisses it. Both resolve every open report of the same comment.

GET/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

PUT/v1/site/users/:userId/card

Updates 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' } }
});
PUT/v1/site/users/cards

The 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)
POST/v1/site/users/:userId/entitlements GET

Grants 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 });
DELETE/v1/site/users/:userId

The person deleted their account on your site: call this along with it. Qorum erases, all at once:

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

GET/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.

GET/v1/site/usage?days=30

Meters 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, ... } }
MeterCounts
readsLists, counts, config, single comment, "me" and notifications.
writesCommenting, replying, voting, editing, deleting, reporting, pinning, uploading images and moderating from the widget.
commentsComments and replies sent, including those that went to the queue.
loginsLog ins and sign ups with a Qorum account.
managementCalls from the dashboard and from this API.
webhook_ok, webhook_errDelivered notices and failed attempts.
deniedRefused requests: domain not on the list, expired token, wrong secret key.

Settings

GET/v1/site/settings PUT

On 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 fieldValues
templateclassico, cartoes, bolhas, compacto, minimalista
temaauto, claro (light), escuro (dark)
cor1, cor2Colors in #rrggbb format
raioCorners, from 0 to 20
ordemrecent, best, hot, oldest, likes_desc
mostrarNaoCurtitrue or false (dislike button)
loginSite, loginQorumWhich logins are valid. If both end up false, the Qorum account is turned on.
urlLoginWhere the Log in button goes when only your site's login is valid
preModeracaonao (off), links (link or image), tudo (everything)
permitirImagenstrue or false
palavrasBloqueadasList of texts (blocked words)
reacoes, permitirVideos, permitirGifstrue or false (default: true)
apiBuscaAddress 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

GET/v1/site/keys POST/v1/site/keys/rotate

Shows and rotates the keys. Rotating creates a new secret key and a new webhook secret, and the old ones stop working right away.

POST/v1/site/webhook/test

Sends a ping event right now. Returns {"ok": true, "status": 200, "ms": 84}, or the error.

POST/v1/site/diagnostics/token

Explains 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", ... } }
GET/v1/site/moderators POST DELETE/:accountId

The 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.

POST/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 }

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);
});