Documentação.
O contrato da API: o formato das chamadas e, principalmente, os seis limites que ela não permite contornar.
No ar
A API responde em https://libellula.dev/api/v1. Não há host separado — api.libellula.dev não resolve, e não vamos publicar um nome só para ele responder erro.
A chave é criada em /app/integracoes/api e aparece uma vez. O que fica no banco é o sha256 dela: um dump vazado entrega a lista de chaves e nenhuma chave. Não há “revelar de novo”, porque poder revelar significaria guardar.
As rotas
Seis endereços, todos de leitura. Cada um exige um escopo, e o índice diz quais a sua chave alcança — para você descobrir isso numa chamada em vez de numa recusa.
| Rota | Escopo | O que devolve |
|---|---|---|
| GET /api/v1 | — | O índice. Devolve as rotas e diz quais a sua chave alcança. |
| GET /api/v1/ativos | assets | Ativos do inventário, com estado e prova de propriedade. |
| GET /api/v1/achados | findings | Achados, com estado, severidade e CVE quando houver. |
| GET /api/v1/prioridades | findings | A fila de risco, com o score e a razão dele. |
| GET /api/v1/autorizacoes | authorizations | Autorizações de coleta, com janela e escopo. |
| GET /api/v1/trilha | audit | A trilha de auditoria — o envelope do evento, nunca o conteúdo. |
Teto de 120 requisições por minuto, por chave, aplicado pelo banco na mesma escrita que confere a credencial — separar conferir de contar abriria a janela em que um teto deixa de valer sob carga, que é quando ele importa. Estourar devolve 429; credencial ruim devolve 401. A distinção existe porque quem estourou o teto deve tentar de novo, e quem tem credencial ruim não deve.
Formato das chamadas
Cabeçalho de credencial, paginação por cursor, e um envelope de erro só.
Listar achados confirmados
curl https://libellula.dev/api/v1/achados \ -H "Authorization: Bearer $LIBELLULA_KEY" \ -G \ -d estado=confirmed \ -d limite=50
severidade nula não é omissão: é o estado do produto. Achado suspeito não tem severidade, e o banco recusa que tenha. Quem vir null está vendo a regra, não um buraco.
Paginação por cursor
# A resposta traz o cursor da próxima página em "proximo".
# Nulo quer dizer que acabou.
{
"itens": [ … ],
"proximo": "FND-4471"
}
curl https://libellula.dev/api/v1/achados \
-H "Authorization: Bearer $LIBELLULA_KEY" \
-G -d limite=50 -d depois=FND-4471Cursor, e não ?pagina=3. Deslocamento mente quando linhas entram ou saem entre uma página e a seguinte: você pula registros e não tem como saber. Com cursor, o pior caso é repetir um — que é visível.
O envelope de erro
{
"erro": {
"codigo": "sem_escopo",
"mensagem": "Esta chave não tem o escopo \"findings\"."
}
}Um formato só, inclusive quando a plataforma quebra. Isto apareceu rodando: uma rota foi ao ar com nome de coluna errado e respondeu 500 sem corpo nenhum — nada vazou, e nada explicou. O detalhe do erro vai para o nosso log; descrever o esquema para quem chama seria dar o mapa a quem tem a chave de uma porta.
Verificar assinatura de webhook
// O motor assina o envelope cru, e nada mais.
const cru = JSON.stringify({
org, seq, action, target, severity, occurred_at
});
const esperado = "sha256=" + crypto
.createHmac("sha256", segredo)
.update(cru)
.digest("hex");
crypto.timingSafeEqual(
Buffer.from(esperado),
Buffer.from(req.headers["x-libellula-signature"])
);O que sai no webhook é o envelope do evento — organização, sequência, ação, alvo, severidade e instante. Nunca o payload: ele pertence à organização e, em eventos de revelação, descreve acesso a dado pessoal.
A única porta que escreve, e por que ela não é a v1
Ingestão de evento de sensor. Credencial própria, um verbo, e nenhum GET.
Enviar evento reduzido
curl -X POST https://libellula.dev/api/sensor \
-H "Authorization: Bearer $LIBELLULA_SENSOR" \
-H "Content-Type: application/json" \
-d '{ "eventos": [
{ "em": "2026-08-30T14:22:07Z",
"classe": "admin-inexistente",
"regra": "waf:sqli-generic",
"origem": "9f3a1c4e77b20de1" }
] }'
# { "gravados": 1, "recusados": [] }O que ela recebe, e o que recusa
O instante, a classe do caminho, a regra que disparou no seu equipamento e a origem como resumo salgado. Nunca a linha de log, nunca o corpo, nunca o agente, nunca o endereço — a restrição de formato da coluna recusa um IP inclusive vindo de um sensor mal configurado.
Campo desconhecido é recusado com o nome dele. Um campo ignorado em silêncio é um vazamento que ninguém descobre: quem mandou acredita que chegou, e quem recebeu acredita que não veio.
Lote de até 200 eventos por chamada, com resposta parcial — um evento inválido no meio não derruba os outros. Não há GET: uma credencial de sensor que também lesse transformaria “alguém inventa evento” em “alguém exporta o padrão de tráfego da organização”.
O que responde sem credencial
Um endereço, e ele é aberto pelo mesmo motivo que uma chave pública existe.
A chave de verificação de exportação
# A metade pública, servida sem autenticação. curl https://libellula.dev/api/assinatura # Confira uma exportação assinada: serialize o campo "conteudo" # do próprio arquivo, compare o sha256 e verifique a assinatura # Ed25519 sobre ele. O cabeçalho está dentro do conteúdo assinado.
Por que sem login
Quem confere uma exportação assinada é o auditor externo, o cliente do nosso cliente, o jurídico do outro lado — gente sem conta aqui. Uma assinatura que exigisse login para ser verificada não serviria ao caso de uso que a justifica.
O que sai é material público por definição: os 32 bytes da metade pública de um par Ed25519. Rotacionar a chave mantém a anterior publicada — exportação assinada verifica só com a chave que a assinou, e despublicar a antiga transformaria em papel sem valor tudo o que já saiu.
Limites explícitos
O que a API não permite, documentado com o mesmo cuidado do que ela permite.
A v1 lê. Não escreve.
Nenhuma rota de /api/v1 cria, altera ou apaga — e isso não é etapa de implementação, é a decisão. Escrita por API é onde o dano acontece, e uma chave vazada que só lê é um incidente de confidencialidade, não de integridade. O dia em que houver caso de uso real para escrever, ele entra numa v2 com o cuidado que merece.
Não existe escopo que revele dado pessoal
evidence_pii não é concedível, e a recusa está numa restrição do banco, não numa validação de formulário. Nenhuma rota devolve conteúdo de evidência. A revelação tem cerimônia própria na interface — finalidade declarada, janela de 60 segundos, limite diário e evento de trilha de severidade alta —, e uma chave de API é exatamente o caminho que dissolveria essa cerimônia sem ninguém notar.
Não existe rota de edição ou exclusão na trilha
A trilha é append-only para qualquer papel e qualquer credencial, incluindo o proprietário da organização. Não há rota porque não há função: o privilégio não existe no banco.
A chave não aciona nem reativa o kill-switch
Nenhuma das duas coisas passa por aqui. Acionar e reativar são atos de interface, com nome do alvo digitado e motivo obrigatório — e o nível global exige duas assinaturas de operadores distintos para se sustentar.
A chave não aprova simulação
A dupla aprovação exige duas pessoas distintas, e a chave conta como a pessoa que a criou. Ela não pode ser a segunda de si mesma; a chave primária da tabela de aprovação impede.
A chave age como uma pessoa, e a trilha continua com um nome
Uma credencial que agisse como “sistema” criaria uma segunda categoria de autor na auditoria, e a primeira pergunta de qualquer investigação — “quem criou essa chave?” — passaria a ser respondida em outro lugar. Toda chave carrega a pessoa que a criou.
Documentar apenas o que a API permite deixa a pergunta mais importante sem resposta: o que acontece se alguém tentar contornar o escopo por integração. A resposta é que nada da v1 enfileira coleta — e, no dia em que algo enfileirar, a revalidação continuará no motor, não na camada de API. É por isso que nenhum destes seis limites depende de a chamada ser bem-comportada.