Skip to content

Public API

The same API the widget uses. With it you build the comments in your own design, in an app or anywhere that can make HTTP requests.

The basics

const QORUM = 'https://useqorum.com';
const KEY = 'pk_YOUR_PUBLIC_KEY';

async function qorum(method, path, body, token) {
  const r = await fetch(QORUM + path, {
    method,
    headers: {
      'X-Qorum-Key': KEY,
      'X-Qorum-Lang': 'en',
      ...(token ? { Authorization: 'Bearer ' + token } : {}),
      ...(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;
}

The comment

Every route that returns comments uses this format:

{
  "id": 81,
  "content": "Great **post**!",
  "html": "Great <strong>post</strong>!",
  "images": [{ "url": "https://i.ibb.co/...", "thumb": "https://i.ibb.co/...", "width": 1200, "height": 800 }],
  "userId": 123,
  "user": { "id": 123, "username": "ana_lima", "displayName": "Ana Lima", "avatar": "https://..." },
  "likes": 12, "dislikes": 0, "hasLiked": true, "hasDisliked": false,
  "replyCount": 2, "isEdited": false,
  "createdAt": "2026-11-08T21:00:00Z",
  "replies": [ { "id": 82, "parentId": 81, "replyToUserId": 123, "replyToUsername": "Ana Lima", ... } ]
}
FieldWhat it is
contentThe text as the person wrote it.
htmlThe text ready to put on the page: everything escaped, with bold, italic, strikethrough, links and <span class="spoiler"> for hidden text. You can use it directly with innerHTML.
imagesAttached images (only present when there are any). thumb is the thumbnail.
userIdThe person's id on your site, or q:N for a Qorum account. Ids made only of digits arrive as numbers.
userThe card your site sent at login, always with id and username.
hasLiked, hasDislikedWhether the reader liked it. Without login, always false.
replyToUserId, replyToUsernameOn replies: who was answered.
pinnedUntilOnly when pinned: until when.
statusOnly shows up as "pending", right after commenting, when the comment went to the queue.
repliesOnly on main comments: up to 50 replies, oldest first.

Read

GET/v1/config

The site's appearance and rules, for you to follow in your interface.

{ "site": "My Blog", "template": "classico", "tema": "auto", "cor1": "#6b2fe0", "cor2": "#2a8ad8", "raio": 8,
  "ordem": "recent", "mostrarNaoCurti": true, "loginSite": true, "loginQorum": true, "urlLogin": "",
  "preModeracao": false, "imagens": true, "imagemMaxMB": 32 }
GET/v1/comments?ref=post-42

The comments of a conversation, page by page. A conversation with no comments yet returns an empty list, not an error.

ParameterDefault
refRequired. The conversation.
page1Page.
limit20Main comments per page, up to 50.
sortthe dashboard'srecent, oldest, likes_desc, likes_asc, dislikes_desc, dislikes_asc.
{
  "comments": [ ... ],
  "pinned": [ ... ],
  "pagination": { "page": 1, "limit": 20, "total": 37, "totalRootComments": 21, "totalPages": 2 },
  "thread": { "ref": "post-42", "title": "My post", "url": "https://...", "locked": false }
}

pinned only comes on page 1 and only when there's a pinned comment. Pinned comments also appear in comments, in their normal position: hide the duplicates if you show both. total counts replies; totalPages counts only main comments.

GET/v1/comments/:id

One comment, for direct links. It always returns the main comment with its replies, and in focusId the one requested (which may be one of the replies).

{ "comment": { "id": 81, ..., "replies": [ ... ] }, "focusId": 82, "thread": { "ref": "post-42", ... } }
GET/v1/comments/:id/replies?offset=50&limit=50

More replies of a main comment, beyond the 50 that come in the list. limit goes up to 100.

{ "replies": [ ... ], "total": 73, "offset": 50 }
GET/v1/counts?refs=post-1,post-2,post-3

How many comments each conversation has, to show "12 comments" in a list of posts. 1 to 100 refs per request.

{ "counts": { "post-1": 12, "post-2": 0, "post-3": 5 } }

Who is logged in

GET/v1/me

Data about whoever is logged in on this site.

{ "userId": 123, "user": { ... }, "isMod": false, "qorumAccount": false,
  "ban": null,                                 // or { "reason", "isPermanent", "expiresAt" }
  "entitlements": [ { "item": "fixar_comentario", "quantity": 2, "expiresAt": null } ],
  "unread": 3 }                                // unread notifications
GET/v1/notifications

The replies the logged-in person received (default 30, up to 100, with ?unread=1 and paging). Everything about notifications, including how to bring them into your site, in Notifications.

{ "notifications": [ { "id": 9, "commentId": 82, "read": false, "createdAt": "...",
    "preview": "I agree, and...", "actor": { "username": "bia" },
    "thread": { "ref": "post-42", "title": "My post", "url": "https://..." } } ],
  "unread": 1 }
POST/v1/notifications/read

Marks as read. Body {"ids": [9, 10]}, or empty to mark all.

Write

All of them need login.

POST/v1/comments

Comments or replies. The first time someone comments on a ref, the conversation is created.

Field
refRequired. The conversation.
content3 to 2000 characters. Can be empty if there's an image.
parentIdTo reply: the id of the comment being answered. A reply to a reply sticks to the main comment.
replyToUserIdWho was answered. Default: the author of the comment in parentId.
imagesUp to 4 addresses returned by POST /v1/images for this same person.
title, urlName and address of the conversation. Stored with the first comment.
await qorum('POST', '/v1/comments', { ref: 'post-42', content: 'Nice!', parentId: 81 }, token);
// 201: the created comment. With "status": "pending", it went to the approval queue.

Common errors: 400 text too short or too long; 403 locked conversation or banned person (with isBanned, reason, isPermanent, expiresAt); 404 the answered comment doesn't exist; 429 commented less than 5 seconds ago or hit a spam limit.

POST/v1/images

Uploads an image to attach to a comment. A multipart/form-data form with the file in the image field: JPG, PNG, GIF or WebP, up to 32 MB. Then send the returned url in images when commenting. The image is converted to WebP (animated GIFs stay animated) and scaled down to at most 2560 pixels on the longest side; width and height already come with the final size.

const fd = new FormData();
fd.append('image', file);                        // from an <input type="file">
const r = await fetch('https://useqorum.com/v1/images', {
  method: 'POST',
  headers: { 'X-Qorum-Key': KEY, Authorization: 'Bearer ' + token },   // no Content-Type
  body: fd,
});
const img = await r.json();   // { "url", "thumb", "medium", "width", "height" }

Errors: 400 format not accepted or resolution over 40 megapixels; 403 images turned off on the site; 413 larger than 32 MB; 429 more than 20 images in 10 minutes.

PUT/v1/comments/:id

Edits your own comment: {"content": "..."}. Returns {"id", "content", "html", "isEdited": true}.

DELETE/v1/comments/:id

Deletes your own comment (moderators delete anyone's). Deleting a main comment deletes its replies.

POST/v1/comments/:id/like and /dislike

Like and dislike. Sending it again undoes it. Liking removes the dislike and vice versa.

{ "liked": true, "likes": 13, "dislikes": 0 }       // /dislike returns "disliked"
POST/v1/comments/:id/report

Reports: {"reason": "Spam or advertising"}. You can't report your own (400) or report twice (409).

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

Pins and unpins. Moderators send {"hours": 48} (default 24). The author pins their own for 24 hours if they have the fixar_comentario perk. Main comments only.

Reactions, follow, block and friends

GET/v1/reactions?ref={"kinds": [...], "counts": {"amei": 12, ...}, "total": 55, "mine": "haha"}
POST/v1/reactions{"ref", "kind"}. The same reaction again (or an empty kind) removes it. Login.
GET/v1/follow?ref={"following": true, "followers": 8}
POST DELETE/v1/followFollow {"ref"} and unfollow ?ref=. Up to 2000 conversations per person.
GET POST/v1/blocksWho the person blocked / block {"userId"} (up to 500). The comment list already comes without blocked people.
DELETE/v1/blocks/:userIdUnblock.
GET/v1/comments/pulse?ref=&after=IDHow many comments arrived after the highest id you have: {"new": 3, "last": 912}. Light: fine to call every 20 s.
GET/v1/users/search?q=an@ autocomplete: up to 8 people of the site. Login.
GET/v1/links/search?q=solo# autocomplete: your site's pages (through the search API). Login.
GET/v1/gifs?q=cat&page=1GIF search (no q = trending). Login.
POST/v1/gifs{"url"} of a GIF from the search: returns the image to send in images when commenting.
GET/v1/mod/comments/:id/historyModerator: previous versions of an edited comment.

On comments: sort=best and sort=hot in the list; the embeds field carries the YouTube videos mentioned ({"type": "youtube", "id", "thumb"}); in html, [text](url) becomes a link and @username becomes <span class="mencao">.

Qorum account

To build your own login screen. It only works if Qorum accounts are turned on for the site.

POST/v1/auth/register
POST/v1/auth/login
// sign up: { "username": "ana_lima", "password": "a good password", "name": "Ana Lima" }   (name optional)
// log in:  { "username": "ana_lima", "password": "a good password" }
// answer:  { "token": "eyJ...", "userId": "q:12", "user": { "username": "ana_lima", "displayName": "Ana Lima" } }

The token lasts 30 days and goes in Authorization: Bearer. Errors: 400 username taken or breaking the rules; 401 wrong username or password; 403 Qorum accounts turned off on the site; 429 more than 10 attempts per minute.

Moderating from the widget

With a moderator login (mod: true in your site's token, or a Qorum account that moderates the site):

GET/v1/mod/reportsReports (?status=pending, reviewed, dismissed or all).
PUT/v1/mod/reports/:id{"status": "reviewed" or "dismissed", "deleteComment": true}
GET/v1/mod/bansWho is banned right now.
POST/v1/mod/bans{"userId", "reason", "hours"} or "permanent": true.
DELETE/v1/mod/bans/:userIdUnbans.
PUT/v1/mod/threads/lock{"ref", "locked": true}
POST/v1/mod/comments/:id/approveApproves a comment from the queue.

Testing the connection

GET/v1/ping?key=pk_...&origin=https://mysite.com

Says whether the key exists, whether the origin is allowed and which logins are on. It isn't blocked by origin, precisely so you can diagnose. See the ready-made test in Troubleshooting.