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
- Base address:
https://useqorum.com - Format: JSON both ways. Dates in ISO 8601, in UTC.
- Key: every request carries the public key in the
X-Qorum-Keyheader (or?key=in the URL). - Login: actions by logged-in people carry
Authorization: Bearer TOKEN, with the token from your site's login or from a Qorum account. Without it, you can only read. - Origin: requests made from a browser must come from a domain allowed for the site. Requests from your server (with no
Originheader) go straight through. - Errors: always
{"error": "message"}, with a message that can be shown to the person. SendX-Qorum-Lang: ento get the messages in English. See the codes in Troubleshooting.
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", ... } ]
}
| Field | What it is |
|---|---|
content | The text as the person wrote it. |
html | The 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. |
images | Attached images (only present when there are any). thumb is the thumbnail. |
userId | The person's id on your site, or q:N for a Qorum account. Ids made only of digits arrive as numbers. |
user | The card your site sent at login, always with id and username. |
hasLiked, hasDisliked | Whether the reader liked it. Without login, always false. |
replyToUserId, replyToUsername | On replies: who was answered. |
pinnedUntil | Only when pinned: until when. |
status | Only shows up as "pending", right after commenting, when the comment went to the queue. |
replies | Only on main comments: up to 50 replies, oldest first. |
Read
/v1/configThe 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 }
/v1/comments?ref=post-42The comments of a conversation, page by page. A conversation with no comments yet returns an empty list, not an error.
| Parameter | Default | |
|---|---|---|
ref | Required. The conversation. | |
page | 1 | Page. |
limit | 20 | Main comments per page, up to 50. |
sort | the dashboard's | recent, 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.
/v1/comments/:idOne 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", ... } }
/v1/comments/:id/replies?offset=50&limit=50More replies of a main comment, beyond the 50 that come in the list. limit goes up to 100.
{ "replies": [ ... ], "total": 73, "offset": 50 }
/v1/counts?refs=post-1,post-2,post-3How 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
/v1/meData 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
/v1/notificationsThe 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 }
/v1/notifications/readMarks as read. Body {"ids": [9, 10]}, or empty to mark all.
Write
All of them need login.
/v1/commentsComments or replies. The first time someone comments on a ref, the conversation is created.
| Field | |
|---|---|
ref | Required. The conversation. |
content | 3 to 2000 characters. Can be empty if there's an image. |
parentId | To reply: the id of the comment being answered. A reply to a reply sticks to the main comment. |
replyToUserId | Who was answered. Default: the author of the comment in parentId. |
images | Up to 4 addresses returned by POST /v1/images for this same person. |
title, url | Name 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.
/v1/imagesUploads 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.
/v1/comments/:idEdits your own comment: {"content": "..."}. Returns {"id", "content", "html", "isEdited": true}.
/v1/comments/:idDeletes your own comment (moderators delete anyone's). Deleting a main comment deletes its replies.
/v1/comments/:id/like and /dislikeLike and dislike. Sending it again undoes it. Liking removes the dislike and vice versa.
{ "liked": true, "likes": 13, "dislikes": 0 } // /dislike returns "disliked"
/v1/comments/:id/reportReports: {"reason": "Spam or advertising"}. You can't report your own (400) or report twice (409).
/v1/comments/:id/pin and DELETEPins 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/follow | Follow {"ref"} and unfollow ?ref=. Up to 2000 conversations per person. |
| GET POST | /v1/blocks | Who the person blocked / block {"userId"} (up to 500). The comment list already comes without blocked people. |
| DELETE | /v1/blocks/:userId | Unblock. |
| GET | /v1/comments/pulse?ref=&after=ID | How 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=1 | GIF 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/history | Moderator: 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.
/v1/auth/register/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/reports | Reports (?status=pending, reviewed, dismissed or all). |
| PUT | /v1/mod/reports/:id | {"status": "reviewed" or "dismissed", "deleteComment": true} |
| GET | /v1/mod/bans | Who is banned right now. |
| POST | /v1/mod/bans | {"userId", "reason", "hours"} or "permanent": true. |
| DELETE | /v1/mod/bans/:userId | Unbans. |
| PUT | /v1/mod/threads/lock | {"ref", "locked": true} |
| POST | /v1/mod/comments/:id/approve | Approves a comment from the queue. |
Testing the connection
/v1/ping?key=pk_...&origin=https://mysite.comSays 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.