Pular para o conteúdo

API de gerência

Tudo o que o painel faz, para o seu servidor. Use para moderar de dentro do painel administrativo do seu site, sincronizar o cartão dos usuários, liberar benefícios e acompanhar os números.

Acesso

Todo pedido leva as duas chaves do site:

X-Qorum-Key: pk_SUA_CHAVE_PUBLICA
X-Qorum-Secret: sk_SUA_CHAVE_SECRETA
Só no servidor.

Essas chamadas não funcionam de um navegador e nunca devem ser feitas de um. Quem tem a chave secreta tem o poder do dono do site.

Chave secreta errada devolve 401 "Chave secreta inválida". Isso acontece logo depois de trocar as chaves no painel, até você atualizar o seu servidor.

// Node 18+: um ajudante para as chamadas abaixo
async function gerencia(metodo, caminho, corpo) {
  const r = await fetch('https://useqorum.com/v1/site' + caminho, {
    method: metodo,
    headers: {
      'X-Qorum-Key': process.env.QORUM_PUBLICA,
      'X-Qorum-Secret': process.env.QORUM_SECRET,
      ...(corpo ? { 'Content-Type': 'application/json' } : {}),
    },
    body: corpo ? JSON.stringify(corpo) : undefined,
  });
  const dados = await r.json();
  if (!r.ok) throw new Error(dados.error);
  return dados;
}

Comentários

GET/v1/site/comments

Todos os comentários do site, do mais novo para o mais antigo, 25 por página (ou o que você pedir em limit). Os filtros se combinam.

Parâmetro
statusapproved (padrão), pending (fila), deleted ou all.
qBusca no texto.
refSó de uma conversa.
refsDe várias conversas, separadas por vírgula (até 500). Ex.: refs=post-1,post-2.
refPrefixDe todas as conversas cujo ref começa assim. Ex.: refPrefix=produto: pega produto:12, produto:80... Dá para separar seções do site só pelo jeito de montar o ref.
userSó de uma pessoa (o id no seu site, ou q:N).
pagePágina.
limitItens por página, de 1 a 100 (padrão 25).
{ "comments": [ { "comment": { ...comentário... }, "status": "pending", "deleted": false, "openReports": 0,
                  "canRestore": false,
                  "thread": { "ref": "post-42", "title": "Meu post", "url": "https://..." } } ],
  "total": 4, "page": 1, "totalPages": 1 }

canRestore é true nos apagados que ainda podem voltar. Os apagados junto com a conta da pessoa (veja Apagar os dados de alguém) ficam sem texto e não voltam.

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

Aprova um comentário da fila. Na hora, ele aparece, conta no total, notifica quem foi respondido e dispara o webhook comment.created.

DELETE/v1/site/comments/:id

Apaga qualquer comentário. Para um comentário da fila, isso é recusar. Apagar um comentário principal leva junto as respostas dele.

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

Desfaz o apagar. O comentário volta, e se for um principal, voltam com ele as respostas que saíram junto. Respostas que já tinham sido apagadas antes, uma por uma, continuam apagadas. Os contadores são refeitos e o seu servidor recebe o webhook comment.restored.

// resposta
{ "message": "Comentário restaurado", "restoredReplies": 2 }
GET/v1/site/comments/:id/history

As versões anteriores de um comentário editado, da mais nova pra mais antiga: {"comment": {...}, "edits": [{"content", "html", "editorId", "createdAt"}]}. Edições feitas antes de 09/11/2026 não têm versão guardada.

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

Fixa por {"hours": N} (padrão 24, máximo 8760) e desafixa.

Conversas

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

As conversas do site, das mais movimentadas recentemente. q busca no ref e no título.

{ "threads": [ { "ref": "post-42", "title": "Meu 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} tranca; false destranca.

Denúncias e banimentos

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

status: pending (padrão), reviewed, dismissed ou all. Aceita os mesmos ref, refs, refPrefix e limit (1 a 100, padrão 20) da lista de comentários.

{ "reports": [ { "id": 5, "commentId": 81, "reason": "Spam ou propaganda", "status": "pending", "createdAt": "...",
                 "content": "texto denunciado", "commentDeleted": false, "threadRef": "post-42", "threadUrl": "https://...",
                 "author": { ...cartão... }, "reporter": { ...cartão... } } ],
  "total": 1, "page": 1, "totalPages": 1 }
PUT/v1/site/reports/:id

{"status": "reviewed", "deleteComment": true} apaga o comentário; {"status": "dismissed"} ignora. As duas resolvem todas as denúncias abertas do mesmo comentário.

GET/v1/site/bans POST/v1/site/bans DELETE/v1/site/bans/:userId
// banir por 7 dias
await gerencia('POST', '/bans', { userId: '123', reason: 'Spam repetido', hours: 168 });
// para sempre
await gerencia('POST', '/bans', { userId: '123', reason: '...', permanent: true });

Usuários

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

Atualiza na hora o nome e o cartão de alguém (trocou a foto, ganhou um selo). Sem isso, o cartão só muda no próximo login. Os comentários na tela podem levar até 30 segundos para mostrar o novo cartão.

await gerencia('PUT', '/users/123/card', {
  name: 'Ana Lima',
  card: { username: 'ana_lima', avatar: 'https://.../nova-foto.jpg', rank: { name: 'Assinante', nameColor: '#7c3aed' } }
});
PUT/v1/site/users/cards

O mesmo, para muitas pessoas de uma vez: até 1000 por pedido. O corpo tem o mesmo formato da resposta da API de dados, então dá para reaproveitar o código. Use numa migração (para o nome e a foto de quem comentou no sistema antigo aparecerem desde a primeira visita) ou depois de uma mudança que atinge muita gente (um selo novo para todos os assinantes).

await gerencia('PUT', '/users/cards', {
  users: {
    '123': { name: 'Ana Lima', card: { username: 'ana_lima', avatar: 'https://.../ana.jpg' } },
    '456': { name: 'Bruno', card: { username: 'bruno' } }
  }
});
// resposta: { "saved": 2, "ignored": 0 }   (ignored = id inválido)
POST/v1/site/users/:userId/entitlements GET

Libera benefícios. Hoje existe fixar_comentario (a pessoa fixa o próprio comentário por 24 horas, gastando uma unidade). A quantidade é somada à que a pessoa já tem; -1 é ilimitado; hours define até quando vale.

// assinante ganhou 3 usos que valem por 30 dias
await gerencia('POST', '/users/123/entitlements', { item: 'fixar_comentario', quantity: 3, hours: 720 });
DELETE/v1/site/users/:userId

A pessoa apagou a conta no seu site: chame isto junto. O Qorum apaga, de uma vez:

Os contadores das conversas e dos comentários são refeitos. Não dá para desfazer.

await gerencia('DELETE', '/users/123');
// resposta: { "deletedComments": 14, "deletedReplies": 3, "removedVotes": 52 }

Notificações

Listar, contar e marcar como lidas as notificações de um usuário, e o feed para copiar tudo para o seu sistema: veja Notificações.

Números

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" }

Os totais de comentários ficam guardados por até 1 minuto (updatedAt diz quando foram contados); ?fresh=1 conta de novo. Pendentes, denúncias e avisos são sempre na hora.

GET/v1/site/usage?days=30

Medidores por dia, de 1 a 365 dias. O dia de hoje inclui o que aconteceu até agora.

{ "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, ... } }
MedidorConta
readsListas, contagens, configuração, comentário único, "eu" e notificações.
writesComentar, responder, votar, editar, apagar, denunciar, fixar, enviar imagem e moderar pelo widget.
commentsComentários e respostas enviados, incluindo os que foram para a fila.
loginsEntradas e cadastros com conta Qorum.
managementChamadas do painel e desta API.
webhook_ok, webhook_errAvisos entregues e tentativas que falharam.
deniedPedidos recusados: domínio fora da lista, token vencido, chave secreta errada.

Configurações

GET/v1/site/settings PUT

No PUT, mande só o que quer mudar. Dentro de config, também só os campos que mudam.

await gerencia('PUT', '/settings', {
  allowedOrigins: ['https://meusite.com', 'https://www.meusite.com'],
  webhookUrl: 'https://meusite.com/qorum/webhook',
  config: { template: 'cartoes', preModeracao: 'links', palavrasBloqueadas: ['cassino', 'aposta'] }
});
Campo de configValores
templateclassico, cartoes, bolhas, compacto, minimalista
temaauto, claro, escuro
cor1, cor2Cores no formato #rrggbb
raioCantos, de 0 a 20
ordemrecent, best, hot, oldest, likes_desc
mostrarNaoCurtitrue ou false
loginSite, loginQorumQuais logins valem. Se os dois ficarem falsos, a conta Qorum é ligada.
urlLoginPara onde o botão Entrar leva quando só vale o login do seu site
preModeracaonao, links (link ou imagem), tudo
permitirImagenstrue ou false
palavrasBloqueadasLista de textos
reacoes, permitirVideos, permitirGifstrue ou false (padrão: true)
apiBuscaEndereço da API de busca do seu site (link com #)

Valores fora do permitido não dão erro: voltam para o padrão. O PUT devolve a configuração como ficou, então confira a resposta.

Chaves, webhook e diagnóstico

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

Mostra e troca as chaves. A troca gera uma chave secreta e um segredo de webhook novos, e os antigos param de valer na hora.

POST/v1/site/webhook/test

Manda um evento ping agora mesmo. Devolve {"ok": true, "status": 200, "ms": 84}, ou o erro.

POST/v1/site/diagnostics/token

Explica o que está errado num token de login: {"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_errada", ... } }
GET/v1/site/moderators POST DELETE/:accountId

Moderadores do site. Para adicionar, {"username": "usuario_da_pessoa"} de uma conta Qorum que já existe.

Trazer comentários de outro sistema

Para migrar os comentários que o seu site já tem. Pode rodar quantas vezes quiser: cada comentário é identificado pelo source e pelo legacyId (o id dele no sistema antigo), e o que já entrou é pulado.

POST/v1/site/import
{
  "source": "comentarios_antigos",            // nome do sistema de origem (até 40 caracteres)
  "comments": [
    { "legacyId": 120, "ref": "post-42", "title": "Meu post", "url": "https://...",
      "userId": "7", "content": "Primeiro!", "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": "Concordo", "createdAt": "2025-03-01T12:05:00Z" }
  ]
}
// resposta: { "imported": 2, "skipped": 0, "orphans": 0, "invalid": 0 }

Exemplo: fila de aprovação no seu admin

Duas rotas no seu servidor, usando o ajudante do começo da página. A tela do seu admin lista os pendentes e chama aprovar ou recusar:

app.get('/admin/comentarios/pendentes', soAdmin, async (req, res) => {
  const { comments } = await gerencia('GET', '/comments?status=pending');
  res.json(comments.map(c => ({
    id: c.comment.id,
    autor: c.comment.user.displayName || c.comment.user.username,
    html: c.comment.html,
    pagina: c.thread.url,
  })));
});

app.post('/admin/comentarios/:id/:acao', soAdmin, async (req, res) => {
  const id = Number(req.params.id);
  if (req.params.acao === 'aprovar') await gerencia('POST', `/comments/${id}/approve`);
  else await gerencia('DELETE', `/comments/${id}`);
  res.sendStatus(204);
});