Documentação da API

webhooks

Receba as novidades por push, sem ficar consultando.

Movimentação nova, publicação no diário, vigília com resultado, busca concluída: cada novidade vira um HTTP POST assinado no endpoint do seu sistema. Este guia documenta os eventos, o formato de entrega, a verificação da assinatura e a política de reentrega.

Como ativar

Cadastre o endpoint receptor de duas formas equivalentes: pelo portal, em /app/webhooks, ou pela API, com uma chave que tenha o escopo Gerenciar webhooks (código WEBHOOKS_WRITE):

curl -X POST "https://api.lexnode.com.br/v1/webhooks/endpoints" \
  -H "Authorization: Bearer lapi_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://seu-sistema.com.br/webhooks/lexnode",
    "eventTypes": ["PUBLICATION_CREATED", "PROCESS_MOVEMENT_CREATED"]
  }'
# A resposta traz o signingSecret UMA única vez — guarde-o com segurança.

# Cadastrou? Valide na hora, sem esperar o primeiro evento real:
curl -X POST "https://api.lexnode.com.br/v1/webhooks/endpoints/{id}/test" \
  -H "Authorization: Bearer lapi_live_..."
# → dispara uma entrega sintética assinada (type WEBHOOK_TEST) e responde
#   com o status que o seu receptor devolveu.

O signingSecret aparece uma única vez, na criação e na rotação (POST /v1/webhooks/endpoints/{id}/rotate-secret). O PATCH é parcial: enviar só { "isActive": false } desativa sem tocar na URL nem nos eventos assinados. Operações de configuração custam 0 créditos.

O que chega no seu endpoint
POST https://seu-sistema.com.br/webhooks/lexnode
content-type: application/json
user-agent: LexNode-Webhooks/1.0
x-lexnode-event: PUBLICATION_CREATED
x-lexnode-timestamp: 1723050000
x-lexnode-signature: t=1723050000,v1=6f2a1c...9b

{
  "id": "evt_01j...",
  "type": "PUBLICATION_CREATED",
  "occurredAt": "2026-08-07T13:40:00.000Z",
  "projectId": "prj_...",
  "monitorId": "mon_...",
  "payload": { ... }
}

O envelope é sempre o mesmo: id identifica o evento (use para deduplicar), type diz qual dos eventos abaixo chegou e payload traz o conteúdo específico daquele tipo.

assinatura

Verifique a origem antes de processar

Toda entrega vai assinada com HMAC-SHA256 no header x-lexnode-signature: t=<unix>,v1=<hex>. O valor v1 é o HMAC do seu secret sobre a string <timestamp>.<corpo cru>. Receptor sério não processa entrega sem validar:

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: o corpo CRU da requisição (antes de qualquer JSON.parse).
function verifyWebhook(rawBody, signatureHeader, secret) {
  // Tolerante a espaços e a chaves futuras (um eventual v2= é ignorado).
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => {
      const [key, ...rest] = part.trim().split("=");
      return [key, rest.join("=")];
    }),
  );
  const { t, v1 } = parts;
  if (!t || !v1) return false;

  // Janela de replay: rejeite entregas com mais de 5 minutos.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`, "utf8")
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(v1, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Python

import hashlib
import hmac
import time

def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    parts = {}
    for part in signature_header.split(","):
        key, _, value = part.strip().partition("=")
        parts[key] = value  # chaves desconhecidas sao ignoradas

    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1:
        return False
    if abs(time.time() - int(t)) > 300:
        return False

    expected = hmac.new(
        secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, v1)

PHP

function verifyWebhook(string $rawBody, string $signatureHeader, string $secret): bool
{
    $parts = [];
    foreach (explode(',', $signatureHeader) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, null);
        $parts[$key] = $value; // chaves desconhecidas são ignoradas
    }

    $t = $parts['t'] ?? null;
    $v1 = $parts['v1'] ?? null;
    if (!$t || !$v1) return false;
    if (abs(time() - (int) $t) > 300) return false;

    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    return hash_equals($expected, $v1);
}

Sem escrever código

A conferência é uma conta, não um programa: quatro passos que qualquer ferramenta de automação executa com blocos prontos.

  1. 1. Guarde o corpo cru

    Assine exatamente os bytes que chegaram. Se a ferramenta já converteu para objeto, use o campo de corpo bruto (raw body) — reserializar o JSON troca espaços e ordem de chaves, e a assinatura deixa de bater.

  2. 2. Monte a mensagem

    Concatene o t do header, um ponto e o corpo cru: t.corpo. O t vem de x-lexnode-timestamp (o mesmo valor está dentro da assinatura).

  3. 3. Calcule o HMAC-SHA256

    Chave = o signing secret do endpoint. Mensagem = a do passo 2. Saída em hexadecimal minúsculo.

  4. 4. Compare e descarte o atrasado

    O resultado tem de ser igual ao v1 do header. E descarte a entrega se o t estiver a mais de 300 segundos do relógio atual — é a janela de replay.

FerramentaOnde fica o passo 3
MakeFunção sha256 com HMAC key: chave = signing secret, key encoding UTF-8, output encoding Hex.
n8nNó Crypto, ação Hmac, type SHA256, encoding HEX; o secret fica na credencial do nó.
ZapierNão há passo de HMAC pronto: use um Code step em JavaScript com o exemplo de Node acima.

Se a assinatura não bater, o suspeito é sempre o passo 1: o corpo foi remontado no caminho. O botão de entrega de teste no portal manda um evento assinado de verdade para conferir sem esperar publicação nova.

Qual mecanismo dispara o quê?

Três mecanismos de acompanhamento alimentam os webhooks — cada um responde a uma pergunta diferente:

Quero…UseEvento que chega
Quero as publicações da minha OABPOST /v1/monitors (tipo OAB)PUBLICATION_CREATED
Quero toda menção a um termo no diário (tese, substância, empresa)POST /v1/monitors (tipo TERM)PUBLICATION_CREATED
Quero saber quando um processo específico andaPOST /v1/subscriptionsPROCESS_MOVEMENT_CREATED
Quero acompanhar um tema ou tese na jurisprudênciaPOST /v1/publications/text-alertsPUBLICATION_TEXT_ALERT_MATCHED

O monitor de processo (tipo PROCESS) complementa a assinatura: emite MONITOR_SYNC_SUCCEEDED a cada varredura (com o campo changed) e MONITOR_SYNC_FAILED quando a varredura falha. Um alvo que a fonte ainda não indexou não falha: o monitor fica em vigília de aparição (o evento de varredura sai com awaitingAppearance) e a primeira aparição chega com firstAppearance, seguida das movimentações do processo.

catálogo de eventos

Os eventos e seus payloads

Cada endpoint escolhe quais tipos quer receber no campo eventTypes. Os exemplos abaixo mostram o conteúdo de payload de cada tipo.

PUBLICATION_CREATED

Um monitor de OAB ou de termo encontrou uma publicação nova no diário oficial. No monitor de termo, o payload traz também o campo term.

{
  "monitorId": "mon_...",
  "publicationKey": "...",
  "processNumber": "00012345620268260100",
  "formattedProcessNumber": "0001234-56.2026.8.26.0100",
  "tribunalAlias": "tjsp",
  "tribunalCode": "TJSP",
  "availableAt": "2026-08-07",
  "sourceUrl": "https://...",
  "type": "Intimação",
  "excerpt": "Prévia curta do teor...",
  "suspensionSignal": null
}

Publicação sem teor entregue chega só com os metadados: excerpt e suspensionSignal vêm null e o payload traz contentWithheld: true — no topo e em cada item de matches. Três causas levam ao mesmo desfecho: segredo de justiça, bloqueio de exibição pedido por titular terceiro dos autos e linha que já saiu do acervo (o evento fica 180 dias; o teor tem prazo menor). O campo não distingue qual delas foi. A decisão é tomada no momento do envio, não no da emissão: publicação que passou a correr em segredo depois do evento é entregue sem teor, inclusive num reenvio. Abrir a publicationKey nesse caso devolve 404 — repetir não muda a resposta.

PROCESS_MOVEMENT_CREATED

Um processo com assinatura ativa (POST /v1/subscriptions) recebeu movimentações novas.

{
  "canonicalProcessId": "...",
  "tribunalAlias": "tjsp",
  "processNumber": "00012345620268260100",
  "externalRef": "seu-id-interno",
  "newMovementCount": 2,
  "latestMovementKey": "..."
}

externalRef é o identificador que você informou ao assinar o processo.

PUBLICATION_TEXT_ALERT_MATCHED

Uma vigília de texto (POST /v1/publications/text-alerts) encontrou publicações novas para a consulta.

{
  "alertId": "...",
  "externalRef": "seu-id-interno",
  "queryHash": "...",
  "totalNew": 3,
  "matches": [
    {
      "source": "STJ_OPEN_DATA",
      "publicationKey": "...",
      "tribunalSigla": "STJ",
      "publicationDate": "2026-08-07T00:00:00.000Z",
      "headline": "Prévia curta do resultado..."
    }
  ]
}

matches traz uma amostra limitada; o total real está em totalNew. O headline de cada item segue o mesmo sigilo reavaliado no envio: item que passou a correr em segredo chega com headline null.

SEARCH_JOB_COMPLETED

Uma busca assíncrona de processos terminou — com sucesso ou falha terminal.

{
  "jobId": "...",
  "status": "SUCCEEDED",
  "kind": "PROCESS_SEARCH",
  "officeId": "escritorio-01",
  "traceId": "...",
  "total": 12,
  "relation": "eq",
  "processNumbers": ["00012345620268260100"],
  "pollUrl": "/v1/search-jobs/{jobId}"
}

Na falha, status vem FAILED e o payload traz error { class, code, message } no lugar dos resultados.

MONITOR_SYNC_SUCCEEDED

Uma varredura de monitor de processo terminou; changed indica se algo mudou desde a anterior. sourceUpdatedAt e latestMovementAt são o que a fonte disse nesta varredura — quando ela diz ter atualizado o processo e a data do movimento mais recente que conhece (nulos quando a fonte não datou): changed diz se mudou, elas dizem se a fonte está viva ou parada. Na primeira varredura em que um alvo antes ausente aparece na fonte, o payload traz firstAppearance: true.

{
  "monitorId": "mon_...",
  "changed": true,
  "processNumber": "00012345620268260100",
  "formattedProcessNumber": "0001234-56.2026.8.26.0100",
  "tribunalAlias": "tjsp",
  "tribunalCode": "TJSP",
  "snapshotHash": "...",
  "sourceUpdatedAt": "2026-08-05T23:09:21.483Z",
  "latestMovementAt": "2026-07-31T11:31:47.000Z"
}

Monitor em vigília de aparição (alvo que a fonte ainda não indexou) também emite este evento a cada passada, com outro formato: awaitingAppearance: true + indexUpdatedThrough/expectedFrom no lugar de changed/snapshotHash. Trate awaitingAppearance como espera, e firstAppearance como o sinal de que o processo apareceu.

MONITOR_SYNC_FAILED

Uma varredura de monitor falhou. `retryable` separa o que passa sozinho do que só você resolve. PROCESS_NOT_FOUND vale para alvo que a varredura JÁ viu antes e sumiu, ou cuja vigília de aparição desistiu (o índice da fonte passou da data de distribuição informada com folga e nada apareceu) — alvo nunca visto e ainda dentro da janela não falha: espera, com awaitingAppearance no evento de sucesso.

{
  "monitorId": "mon_...",
  "code": "PROCESS_NOT_FOUND",
  "class": "TERMINAL",
  "retryable": false,
  "error": "Process not found."
}
PUBLICATION_HARVEST_COMPLETED

Uma fatia de colheita do diário oficial que você demandou terminou de ser indexada.

{
  "sliceId": "...",
  "tribunalSigla": "TJSP",
  "harvestDate": "2026-08-07",
  "seedTerm": "...",
  "itemsIndexable": 148
}
QUOTA_LOW

O consumo do ciclo passou de 80% do teto disponível (franquia do plano mais créditos avulsos ativos).

{
  "usedCredits": 1680,
  "limitCredits": 2000,
  "includedMonthlyCredits": 2000,
  "topupCredits": 0,
  "remainingCredits": 320,
  "thresholdPercent": 80,
  "periodStart": "2026-08-01T00:00:00.000Z",
  "periodEnd": "2026-09-01T00:00:00.000Z"
}

Avisa uma vez por teto: comprar um pacote muda o teto e devolve o direito a um aviso novo.

PERIOD_ENDING

O ciclo do plano — assinatura ou avaliação — termina em até 7 dias.

{
  "planCode": "STARTER",
  "periodEnd": "2026-09-06T00:00:00.000Z",
  "daysRemaining": 5
}
SUBSCRIPTION_PAST_DUE

A cobrança da assinatura falhou e a conta está em atraso — o acesso é bloqueado enquanto ela não for regularizada.

{
  "status": "PAST_DUE",
  "planCode": "STARTER",
  "currentPeriodEnd": "2026-09-06T00:00:00.000Z"
}

Regularizar e falhar de novo no ciclo seguinte gera um evento novo.

Confirmação e reentrega

Qualquer resposta 2xx confirma a entrega. Qualquer outra resposta — ou timeout de 15 segundos — agenda uma nova tentativa: são até 6 tentativas por entrega, com intervalo que dobra a cada falha — 10 min, 20 min, 40 min, 1h20 e 2h40, cerca de 5 horas no total. Uma recusa definitiva — 404 ou 410 — esgota em 3 tentativas: a rota que não existe não passa a existir com mais horas de insistência. Esgotadas as tentativas, a entrega para: não insistimos indefinidamente. Se o seu receptor estiver fora do ar, esse total é maior, porque espaçamos as tentativas do endpoint — veja abaixo.

Esgotadas as tentativas, a entrega fica marcada como FAILED — visível em GET /v1/webhooks/deliveries e no portal. Para reenviar, use o replay: POST /v1/webhooks/events/{eventId}/replay reenfileira as entregas do evento (o eventId está na listagem de entregas) — inclusive para endpoints cadastrados depois do evento.

O endpoint continua ativo: nós não o desativamos. O que o motor faz, depois de 7 dias de recusa contínua, é suspender o push: nenhuma entrega nova nasce para o endpoint até você retomar. Nada é descartado — a fila que já existia fica como está, todo evento continua disponível por 180 dias e você é avisado por e-mail. Desativar um endpoint (seu, pelo portal ou pela API) é outra coisa: cancela as entregas ainda pendentes dele; reativar não as ressuscita — o replay é a via para isso. O prazo de suspensão conta a partir da publicação desta regra para quem já estava recusando.

Quando o receptor fica fora do ar, o que muda é o ritmo, não o destino. A partir de 5 recusas seguidas no mesmo endpoint, espaçamos as tentativas dele: 15 min, 30 min, 1h, 2h, 4h e o teto de 6 horas. Vencido o intervalo, uma entrega passa como sonda — sempre a mesma entrega, até ela esgotar as tentativas — e qualquer resposta 2xx devolve a fila ao ritmo normal na mesma hora, sem você precisar avisar ninguém. Nenhuma entrega é descartada, os 180 dias de replay não mudam e o endpoint segue ativo; o motor só para de empurrar milhares de requisições contra um servidor que já respondeu que não pode recebê-las. Persistindo a recusa por 7 dias, o push é suspenso (acima).

Você é avisado na própria chamada. Toda entrega leva x-lexnode-delivery-attempt (a tentativa e o teto) e x-lexnode-endpoint-failures (recusas seguidas). Quando a próxima recusa for espaçar a fila — e enquanto ela estiver espaçada —, vai junto x-lexnode-endpoint-warning dizendo o que está para acontecer. A entrega que disparou o espaçamento guarda a explicação no lastError, visível na listagem de entregas e no portal.

Para retomar sem esperar — e para sair da suspensão: corrigido o receptor, o botão “Retomar entregas” no portal — ou POST /v1/webhooks/endpoints/{endpointId}/resume — zera a fieira, desfaz a suspensão e devolve a fila ao ritmo normal no ciclo seguinte. Havendo uma série de recusas, a resposta traz replayFrom, o início dela — o instante de onde reenviar por janela para repor o que falhou e, se o push esteve suspenso, o que nasceu sem entrega; no portal, a retomada já abre o “Reenviar período” nessa data. Ela não ressuscita entrega já esgotada: para isso continua sendo o replay. Enquanto o push estiver suspenso, o reenvio por janela responde 409 e o reenvio por evento pula o endpoint.

Depois de uma pausa longa, reenviar evento por evento não escala. O reenvio por janela — POST /v1/webhooks/endpoints/{endpointId}/replay com { "from": "2026-08-22T00:00:00Z" } — repõe de uma vez tudo o que ocorreu desde o instante pedido: cria as entregas que faltam e devolve à fila as que falharam ou foram canceladas. O que já chegou não é reenviado, a menos que você peça com includeDelivered: true. Cada chamada processa até 500 eventos e responde skipped e nextFrom quando a janela é maior — repita com from = nextFrom para continuar. No portal, é o “Reenviar período” no cartão do endpoint.

Na primeira entrega recusada pelo seu receptor, avisamos por e-mail o titular da conta — uma vez a cada 24 horas por endpoint, não a cada entrega, e sem esperar as tentativas se esgotarem. Com a conta bloqueada por cobrança, as entregas esperam na fila — nada é descartado — e voltam quando a conta regulariza; os avisos de cobrança (QUOTA_LOW, PERIOD_ENDING e SUBSCRIPTION_PAST_DUE) são entregues mesmo durante o bloqueio. Para descobrir o que ficou para trás sem depender de ter recebido a entrega, liste os eventos: GET /v1/events?undeliveredOnly=true devolve o que o motor gerou e nunca chegou a endpoint nenhum, com o eventId que o replay exige.

Por quanto tempo dá para reenviar: o evento fica disponível por 180 dias — é a janela de replay. O histórico de cada tentativa de entrega, com o corpo da resposta do seu receptor, é mantido por 90 dias depois de resolvido; tentativa ainda pendente ou em retentativa nunca é descartada.

Saúde agregada do seu receptor: GET /v1/webhooks/health.

Requisitos do receptor

Responda 2xx em até 15 segundos

Qualquer resposta 2xx confirma o recebimento. Grave o evento e processe depois, de forma assíncrona — o timeout de cada tentativa é de 15 segundos.

Valide a assinatura sobre o corpo cru

Calcule o HMAC antes de qualquer parse do JSON. Frameworks que desserializam o corpo automaticamente precisam expor o corpo bruto para essa rota.

Deduplique pelo id do evento

Uma entrega pode chegar mais de uma vez (por exemplo, se o seu lado respondeu lento). O id do evento é estável — ignore repetições.

Não dependa da ordem de chegada

Reentregas fazem eventos chegarem fora de ordem. Use occurredAt e o estado do seu domínio, não a ordem dos POSTs.

Ignore types que não reconhece

Novos tipos de evento podem surgir — e a entrega de teste chega com type WEBHOOK_TEST. Type desconhecido é no-op: responda 2xx e descarte.

As operações de webhooks (GET/POST/PATCH/DELETE /v1/webhooks/endpoints, rotação de secret, entrega de teste e replay) estão na referência oficial.

Abrir o Swagger UIVoltar à documentação