@lexnode/legal-data-sdk
v1.31.0
Published
Cliente TypeScript do LexNode, o motor de dados jurídicos do Brasil: transporte tipado, erros classificados, verificação de webhook e os tipos gerados do contrato OpenAPI.
Maintainers
Readme
@lexnode/legal-data-sdk
Cliente TypeScript do LexNode, o motor de dados jurídicos do Brasil: processos e movimentações, publicações dos diários oficiais, jurisprudência, monitores e webhooks.
O pacote traz os tipos gerados do contrato OpenAPI e um cliente de runtime construído sobre eles: chamadas com os cabeçalhos certos, erros classificados, verificação de assinatura de webhook, espera de busca assíncrona, leitura do frescor da fonte e a vigília de um processo para vários interessados.
- Sem dependências de runtime.
fetchglobal enode:crypto. - Node >= 20, ESM.
⚠️ Só no servidor, e só por TLS. A credencial é uma chave de conta: no browser ela fica exposta, e em
http://qualquer um no caminho a lê — umabaseUrlsem https é recusada no construtor (exceto no host local, para desenvolvimento). Um app com usuários finais chama o próprio backend, e o backend chama a API, atribuindo cada chamada ao sub-tenant dela (um escritório, um usuário final) pelo cabeçalhoX-Office-Id.
Instalação
npm install @lexnode/legal-data-sdkO cliente
import { createLexNodeClient } from "@lexnode/legal-data-sdk";
const lexnode = createLexNodeClient({
baseUrl: "https://api.lexnode.com.br",
apiKey: process.env.LEXNODE_API_KEY!,
// Escritório padrão; cada chamada pode trocá-lo.
officeId: "escritorio-01",
timeoutMs: 30_000,
userAgent: "meu-app/1.0",
});
const { data, meta } = await lexnode.processes.get("00012345620268260100", {
tribunalAlias: "TJSP",
});
console.log(data.formattedNumber, meta.credits, meta.rateLimit?.remaining);Toda chamada envia Authorization: Bearer, X-Request-Id, X-Trace-Id e,
quando houver, X-Office-Id; respeita o prazo e o signal que você
passar, o que vier primeiro; e devolve { data, meta } tipados pela rota. O
meta é o do contrato mais status (o código HTTP) e rateLimit (os
cabeçalhos RateLimit-*, lidos também no sucesso, e o Retry-After da
recusa).
Em rateLimit, use remaining para desacelerar. resetSeconds repete o
cabeçalho RateLimit-Reset, que tem dois sentidos: na recusa, os segundos até
a janela reabrir; no sucesso, a duração da janela (60 s).
O pacote não valida a resposta em tempo de execução: data e meta chegam
tipados pelo contrato da versão instalada. Se o dado alimenta um banco, por
exemplo, valide o que consumir.
Métodos nomeados e a chamada genérica
Os métodos nomeados cobrem o uso diário:
await lexnode.tribunals.list();
await lexnode.processes.movements(numero, { tribunalAlias });
await lexnode.publications.list({ oabNumber: "123456", oabState: "SP" });
await lexnode.monitors.create({ type: "OAB", oabNumber: "123456", oabState: "SP" });
await lexnode.subscriptions.create({ tribunalAlias, processNumber, externalRef });
await lexnode.searchJobs.get(jobId);
await lexnode.processLookups.get(lookupId);
await lexnode.events.list({ limit: 50 });
await lexnode.webhooks.endpoints.list();
await lexnode.webhooks.unsubscribedEventTypes();
await lexnode.usage.get();Qualquer rota do contrato, inclusive as que não têm método nomeado, sai pela
chamada genérica, tipada por paths:
const { data } = await lexnode.request("/v1/precedents", "get", {
params: { query: { query: "dano moral", limit: 20 } },
});Páginas
A API pagina de três jeitos: cursor no meta, cursor no data, e página e
tamanho ou deslocamento e limite. paginate percorre qualquer um deles, até
maxPages páginas (padrão 100) — cada página é uma chamada:
import { paginate } from "@lexnode/legal-data-sdk";
for await (const page of paginate(lexnode, "/v1/monitors", "get", {})) {
for (const monitor of page) console.log(monitor.id);
}Erros
import { LexNodeError } from "@lexnode/legal-data-sdk";
try {
await lexnode.processes.search({ tribunalAlias, number });
} catch (error) {
if (error instanceof LexNodeError) {
error.code; // "RATE_LIMITED", "PROCESS_NOT_FOUND", …
error.class; // "RETRYABLE" | "TERMINAL"
error.retryable;
error.status;
error.upstreamStatus; // o status que a fonte respondeu, quando informado
error.requestId; // o que o suporte pede
error.fieldErrors; // recusa de validação, por campo
error.retryAfterSeconds;
error.rateLimit;
}
}Prazo esgotado vira CLIENT_TIMEOUT (504, retentável); falha de rede vira
CLIENT_NETWORK_ERROR (502, retentável); resposta fora do formato da API vira
INVALID_RESPONSE (502), com o início do corpo na mensagem; e o pedido que não
se monta — cabeçalho com quebra de linha, corpo que não vira JSON — vira
CLIENT_INVALID_REQUEST (400, não retentável), porque não chegou a ser
enviado.
LEXNODE_ERROR_CODES traz o catálogo para o switch, e LexNodeErrorCode
mantém o autocompletar e aceita um código novo da API. Na leitura de processo
isso importa: a API responde 404 tanto para PROCESS_NOT_FOUND (o processo não
existe na fonte) quanto para MOVEMENTS_NOT_AVAILABLE (ele existe, mas as
movimentações não vieram) — trate os dois pelo code.
Retentativa (opcional, desligada por padrão)
const lexnode = createLexNodeClient({
baseUrl,
apiKey,
retry: { attempts: 3, baseDelayMs: 500 },
});Repete só o que é RETRYABLE, respeitando Retry-After: quando a recusa pede
uma espera maior que maxDelayMs (8 s por padrão), como a da manutenção
declarada pela fonte, ela é lançada sem repetição, e retryAfterSeconds diz
quando vale tentar de novo. GET e DELETE são repetidos por definição do
HTTP. Uma escrita só é repetida quando o contrato declara a repetição
segura — hoje POST /v1/publications/text-searches, que usa o requestId que
você fixar como chave de reenvio — ou quando você mesmo o declara na chamada:
await lexnode.webhooks.endpoints.create(corpo, { idempotent: true });A declaração é sua: sem ela, um POST repetido cria um segundo recurso —
dois endpoints para a mesma URL entregam cada evento em dobro, e o segredo do
primeiro se perde. Nas outras rotas, o requestId só serve para rastrear a
chamada.
A exceção é o UPSTREAM_TIMEOUT da leitura de um processo, que a política não
repete — ver a seção da fonte lenta.
Webhooks
import {
parseWebhookEvent,
webhookRejectionStatus,
} from "@lexnode/legal-data-sdk";
// O corpo como chegou, nunca um JSON serializado de novo: a assinatura vale
// para os bytes.
const resultado = parseWebhookEvent(
rawBody,
request.headers,
process.env.LEXNODE_WEBHOOK_SIGNING_SECRET!,
);
// Cada recusa tem o seu status: a assinatura que não confere é 401; o segredo
// ausente no seu receptor é 503.
if (!resultado.ok) {
return reply.status(webhookRejectionStatus(resultado.reason)).send();
}
// Tipo que o catálogo ainda não descreve: ignore e responda 2xx.
if (!resultado.known) return reply.status(200).send();
switch (resultado.event.type) {
case "PUBLICATION_CREATED":
resultado.event.payload.publicationKey;
resultado.event.payload.excerpt; // null quando o teor foi retido
break;
case "PROCESS_MOVEMENT_CREATED":
resultado.event.payload.externalRef;
resultado.event.payload.degree; // o grau do movimento mais recente
break;
case "PROCESS_INSTANCE_DISCOVERED":
// Outro grau do processo apareceu na fonte; o histórico dele está no
// retrato, em `instances`. O processo retido vem sem o grau.
if (!resultado.event.payload.contentWithheld) {
resultado.event.payload.degree;
}
break;
case "SEARCH_JOB_COMPLETED":
if (resultado.event.payload.status === "FAILED") {
resultado.event.payload.error.code;
}
break;
case "PROCESS_LOOKUP_COMPLETED":
// Aponta a consulta: o processo se lê em payload.pollUrl, sem nova cobrança.
resultado.event.payload.lookupId;
break;
case "WEBHOOK_TEST":
// A entrega de teste (`webhooks.endpoints.test` ou o portal). Chega a
// qualquer endpoint sem constar de `eventTypes`; use-a para validar o
// receptor antes do primeiro evento.
break;
default:
break;
}Entrega sem id ou sem occurredAt é recusada como INVALID_ENVELOPE. A
mesma entrega pode chegar mais de uma vez: use o id para descartar a
repetida.
WEBHOOK_EVENT_TYPES é o catálogo assinável (o que entra em eventTypes
ao cadastrar um endpoint) e não inclui WEBHOOK_TEST, que chega sem ser
pedido — use isWebhookTest(event) para separá-lo.
verifyWebhookSignature(rawBody, headers, secret) faz só a conferência e
devolve o motivo da recusa (MISSING_SIGNATURE, MALFORMED_SIGNATURE,
TIMESTAMP_MISMATCH, TIMESTAMP_OUT_OF_TOLERANCE, SIGNATURE_MISMATCH,
RAW_BODY_REQUIRED), para você tratar cada caso.
O corpo entra como string ou Buffer, como chegou. Um objeto já convertido
de JSON é recusado como RAW_BODY_REQUIRED: serializar de novo muda a ordem
das chaves, e a assinatura deixa de conferir.
A tolerância do instante e o digest usam o t que vem dentro da
assinatura. O cabeçalho x-lexnode-timestamp repete esse valor: quando vem,
precisa ser igual; quando falta, não é motivo de recusa.
Busca síncrona ou assíncrona
POST /v1/processes/search responde 200 com o resultado pronto ou 202
com uma busca enfileirada. O SDK diz qual foi, no tipo:
import { searchProcesses, waitForSearchJob } from "@lexnode/legal-data-sdk";
const desfecho = await searchProcesses(lexnode, { tribunalAlias, number });
if (desfecho.kind === "sync") {
desfecho.result.items;
} else {
// Espere com orçamento — num ambiente serverless, dentro do prazo da
// função — ou receba o SEARCH_JOB_COMPLETED por webhook.
const job = await waitForSearchJob(lexnode, desfecho.job.jobId, {
budgetMs: 20_000,
});
job.status; // "SUCCEEDED" | "FAILED" | "CANCELED"
}Um número, várias instâncias
A fonte processual guarda um documento por instância do processo — 1º grau, 2º grau, turma recursal… —, às vezes dois do mesmo grau, e o LexNode guarda cada um na sua instância.
O topo não troca. O topo da ficha (data) e do retrato (data.process e
data.movements) é sempre a instância principal do número, com o grau em
degree: a mesma de uma leitura para a outra, qualquer que seja a ordem em que
a fonte devolve os documentos. Ela só muda quando uma resposta completa da
fonte pelo número deixa de trazê-la. Se você compara movimentos pela chave, o
histórico de outro grau não aparece como novo no topo — e o topo pode não ser o
grau que anda: o 2º grau guardado primeiro continua no topo enquanto o 1º se
move.
Todos os graus em instances. A ficha e o retrato listam em instances
todos os documentos guardados do número, a principal primeiro
(primary: true). Cada um diz de onde veio nesta resposta (origin: SOURCE
quando a fonte o trouxe, LAKE quando veio da cópia guardada), quando foi
buscado (fetchedAt) e desde quando a resposta completa da fonte vem sem ele
(absentSince). No retrato, cada instância traz os movimentos da última
captura dela, do mais recente ao mais antigo e sem o payload bruto da fonte; a
instância cujos movimentos nunca foram capturados vem com
movementsCaptured: false e movements nulo, nunca com lista vazia, que diria
que o grau não andou. Todos os documentos levam a classificação do número
inteiro — basta um em segredo de justiça para todos saírem marcados —, e a
ficha retida pelo controle de confidencialidade sai sem a lista.
const { data } = await lexnode.processes.snapshot(numero, { tribunalAlias });
for (const instancia of data.instances) {
instancia.degree; // "G1", "G2", "TR"…
instancia.primary; // true só na do topo
instancia.movements ?? "movimentos nunca capturados";
}No lote (processes.snapshotBatch), instances traz só o cabeçalho de cada
documento; os movimentos de cada instância vêm no retrato de um processo. A
busca por número completo respondida com os dados guardados devolve um item por
documento que a fonte ainda traz do número, a principal primeiro; quando eles
não cabem em limit, a busca vai à fonte, que pagina.
Recorte por grau. A lista de movimentações aceita degree e devolve só os
movimentos que o documento daquele grau trouxe na última captura; cada item diz
em degrees o grau — ou os graus, quando o mesmo ato está nos dois documentos
— que o trouxe, e vem vazio no movimento que nenhuma captura atual traz.
const { data } = await lexnode.processes.movements(numero, {
tribunalAlias,
degree: "G1",
});
data.items; // só o que o 1º grau trouxeO grau novo chega num evento só. Quando outro grau de um processo assinado
aparece na fonte, o histórico dele não sai movimento a movimento em
PROCESS_MOVEMENT_CREATED: chega um PROCESS_INSTANCE_DISCOVERED, com o grau,
o órgão julgador e as contagens — historyMovementCount não saiu em aviso
próprio, announcedMovementCount saiu também como movimento novo. O
PROCESS_MOVEMENT_CREATED diz o grau do movimento mais recente (degree,
instanceSourceId) e quantos movimentos novos vieram de cada grau
(instances). Assine os dois: só com o movimento, você não fica sabendo do
grau que apareceu.
meta.otherInstances: o resumo do que veio. Quando a resposta da fonte traz
outros documentos do número além da principal, meta.otherInstances resume cada
um com o grau (grau), a data em que a fonte o atualizou (updatedAt) e o
órgão julgador (courtBody, quando vem). A lista não sai para processo restrito
e não inclui documento em segredo de justiça; os documentos inteiros estão em
instances.
for (const outra of meta.otherInstances ?? []) {
// Ex.: { grau: "G1", updatedAt: "2026-04-15T12:00:00.000Z" }
}Continuação. Na API nada é cortado: o retrato traz os movimentos guardados
de cada instância, e a lista de movimentações lê os movimentos guardados, grau a
grau, sem ir à fonte. No canal para agentes (MCP), que não passa por este
pacote, consultar_processo cabe numa resposta: de cada grau vêm os movimentos
mais recentes, o grau que se moveu por último primeiro, e a mesma tool com
grau e pagina traz os seguintes, por 1 crédito e sem ir à fonte — só para
quem já leu o processo ou o acompanha.
Ler um processo quando a fonte está lenta
A ficha (processes.get) vai à fonte processual quando não há uma cópia recente
guardada; o retrato (processes.snapshot) vai sempre. Os dois só respondem com
a cópia guardada quando a fonte falha (seção seguinte). A fonte às vezes leva
perto de um minuto por consulta. waitSeconds diz quanto a API espera por ela:
de 10 a 180 segundos, padrão 20.
const { data, meta } = await lexnode.processes.get(numero, {
tribunalAlias: "TJSP",
waitSeconds: 120,
});O prazo da chamada acompanha a espera. Sem timeoutMs na chamada, o SDK
usa o maior entre o prazo do cliente e a espera mais 15 s: depois da fonte, a
ficha ainda espera até 10 s pelo resumo das publicações do processo, e o resto
é a rede. Com waitSeconds: 120, a chamada desiste aos 135 s, e não aos 30 s do
padrão; sem waitSeconds, o SDK conta a espera padrão da API, e a chamada
desiste aos 35 s. Um timeoutMs explícito sempre vence. O mesmo vale para a
chamada genérica (request) nessas duas rotas.
O tempo esgotado não se repete sozinho. UPSTREAM_TIMEOUT (504) nessas
duas leituras é lentidão da fonte, não ausência do processo, e a política de
retentativa não o repete: a espera já correu inteira, e repetir na hora a
dobraria diante da mesma fonte lenta. As outras recusas retentáveis seguem a
política. Para insistir, a decisão é sua — uma nova chamada com espera maior, a
leitura que segue em segundo plano (readProcessWithRecovery, abaixo) ou a
vigília do processo (watchProcess).
meta.partialIndex: o processo veio de parte do índice. Quando a fonte
responde com só parte do índice e o processo estava na parte que respondeu, ele
é servido, e meta.partialIndex diz quantas partes falharam (shardsFailed),
de quantas (shardsTotal, nulo quando a fonte não as contou) e se o prazo
interno dela estourou (timedOut). O documento já guardado que não veio sai em
instances com origin: "LAKE" e a data dele, e outro grau ainda não guardado,
se existir, pode estar na parte que não respondeu. A resposta parcial sem o
processo é 502 UPSTREAM_UNAVAILABLE retentável, nunca 404.
if (meta.partialIndex) {
// Um grau deste número ainda não guardado pode não ter vindo.
}Antes de recusar a resposta parcial que veio sem o processo porque partes do índice ficaram fora, a API consulta a fonte de novo — até duas vezes, com uma pausa curta, enquanto a espera couber.
Agentes têm a espera deles. No canal para agentes (MCP), as tools
ler_processo e consultar_processo escolhem a espera em maxWaitSeconds, na
mesma faixa e com padrão de 120 s; esse canal não passa pelo SDK.
Quando a fonte falha: o retrato guardado
Quando a fonte processual falha — não responde dentro de waitSeconds, fica
indisponível, responde com parte do índice sem poder servir o processo ou pede
para esperar por volume — e há uma cópia guardada do processo, a ficha
(processes.get) e o retrato (processes.snapshot) respondem com essa cópia em
vez do erro. A resposta diz que é cópia, e de quando:
meta.lakeRescue.causediz por que a fonte não entregou:UPSTREAM_TIMEOUT,UPSTREAM_UNAVAILABLEouUPSTREAM_RATE_LIMITED;meta.lakeRescue.retryAfterSeconds, repetido no headerRetry-After, diz na recusa por volume quanto esperar antes de consultar a fonte de novo. É a espera pedida pela fonte, não pelo limite de vazão da API:meta.rateLimit.retryAfterSecondsficanullno sucesso;meta.sourceFetchedAtdiz quando a cópia foi buscada na fonte. Esse campo acompanha toda resposta das duas leituras, venha ela da fonte ou da cópia, menos a ficha que o controle de confidencialidade entrega retida, sem o teor.
A cópia custa 1 crédito, na ficha e no retrato. O retrato confere o saldo pelo
preço cheio, 8, antes de ir à fonte; a ficha confere pelo preço do que
entregou, depois da leitura. No retrato guardado, cada instância traz os
movimentos da última vez que ela foi buscada, com a data em
instances[].fetchedAt, e sourcePayload é nulo: o que andou depois dessas
datas não está na resposta. O número que a resposta completa mais recente da
fonte já não trazia em documento nenhum não tem cópia servida. Na ficha
guardada, a data vale para os dados do processo, e o resumo das publicações
(djen) é relido na hora.
Até que idade. Sem pedir nada, só a cópia de até 24 h responde na falha da
fonte — nessas duas rotas, uma resposta de sucesso já pode ser uma cópia
guardada dentro desse prazo. A cópia de qualquer idade vem com
staleOnFailure: true:
import { describeFreshness, lakeRescueOf } from "@lexnode/legal-data-sdk";
const retrato = await lexnode.processes.snapshot(numero, {
tribunalAlias: "TJSP",
staleOnFailure: true,
});
const copia = lakeRescueOf(retrato);
if (copia) {
// A fonte falhou e isto é a cópia guardada: o que andou depois de
// copia.sourceFetchedAt não está aqui.
mostrar(describeFreshness(retrato));
// "cópia guardada em 26/09/2026, servida porque a fonte não respondeu a tempo"
}lakeRescueOf devolve { cause, sourceFetchedAt, retryAfterSeconds } ou
null — sourceFetchedAt é nulo na ficha retida —, e sourceFetchedAtOf
devolve a data de qualquer resposta dessas leituras. As duas aceitam o envelope
inteiro e o item do lote.
Quando a cópia não é servida. A resposta da fonte que não traz o processo segue 404
PROCESS_NOT_FOUND, com cópia guardada ou sem ela: é a fonte dizendo que não
tem o número, e não uma falha dela. A cópia passa pelo mesmo controle de
confidencialidade da leitura; além de 24 h, só a recebe a conta que já tem
interesse registrado no processo — de uma leitura anterior dele ou de uma
assinatura ativa —, e as demais recebem a falha da fonte, como se não houvesse
cópia.
No lote. processes.snapshotBatch só usa a cópia de até 24 h. O item
respondido com ela traz lakeRescue, com a causa e a data, e custa 1 crédito
em vez de 8.
Leitura que não coube na espera
Quando a fonte processual não entrega dentro de waitSeconds, a ficha e o
retrato respondem com o erro dela (ou com a cópia guardada, seção anterior).
processLookups.create faz a MESMA leitura, e o que não coube na espera segue
em segundo plano em vez de virar erro:
- 200: a leitura coube.
dataé uma consultaSUCCEEDEDsem consulta aberta (lookupIdnulo) que traz o processo emprocess(visãoPROCESS) ousnapshot(visãoSNAPSHOT), com as instâncias do número eminstances, o mesmometae o mesmo preço da leitura direta; - 202: a fonte falhou — não respondeu a tempo, ficou indisponível ou pediu
para esperar —, e a API abriu uma consulta que segue tentando sozinha, até
60 s por tentativa, por até 6 h (
giveUpAt). Nada é cobrado na hora: o preço da leitura fica retido emcreditsReserved, o máximo que a entrega cobra, e só é cobrado se o processo vier. Se não vier, a retenção volta; - qualquer outra recusa é a da leitura direta: 404 quando a fonte diz não ter o processo, 403 no segredo de justiça, 402 sem saldo.
Para uma integração que precisa continuar mesmo quando a fonte demora, use
readProcessWithRecovery. A aceitação da cópia antiga é explícita, e a espera
total é limitada. O fim da espera devolve pending, com a mesma consulta e a
próxima leitura útil, sem cancelar a consulta nem abrir outra:
import { readProcessWithRecovery } from "@lexnode/legal-data-sdk";
const leitura = await readProcessWithRecovery(lexnode, {
tribunalAlias: "TJSP",
processNumber: numeroDoProcesso,
view: "PROCESS",
waitSeconds: 5,
staleOnFailure: true, // aceita cópia anterior à falha: confira sourceFetchedAt
}, { completionBudgetMs: 30_000 });
if (leitura.kind === "ready") {
usarProcesso(leitura.data, leitura.meta.sourceFetchedAt);
} else if (leitura.kind === "pending") {
guardarConsulta(leitura.lookup.lookupId, leitura.nextReadAt);
// leitura.stale, quando presente, conserva a data da cópia.
// Continue pelo pollUrl ou pelo webhook PROCESS_LOOKUP_COMPLETED.
} else {
registrarDesfecho(leitura.kind, leitura.lookup.error);
// failed, canceled ou expired; nenhum deles afirma que o processo não existe.
}Sem completionBudgetMs, a função devolve na hora o desfecho da abertura (200
ou 202). O orçamento cobre só o acompanhamento, e cada chamada mantém o próprio
prazo. Abortar a espera não cancela a consulta. O custo, o controle de
confidencialidade e a cobrança única são os de processLookups.create.
lookupProcess diz, no tipo, qual dos dois desfechos aconteceu, e
waitForProcessLookup espera a consulta terminar:
import { lookupProcess, waitForProcessLookup } from "@lexnode/legal-data-sdk";
const desfecho = await lookupProcess(lexnode, {
tribunalAlias: "TJSP",
processNumber: numero,
view: "SNAPSHOT",
});
if (desfecho.kind === "ready") {
desfecho.data.movements; // o retrato, como processes.snapshot o devolveria
} else {
// Espere com orçamento. A consulta é lida de novo na hora da próxima
// tentativa (nextAttemptAt), quando o estado pode mudar, e a cada 10 s
// enquanto uma tentativa está em curso.
const consulta = await waitForProcessLookup(
lexnode,
desfecho.lookup.lookupId!,
{ budgetMs: 10 * 60_000 },
);
if (consulta.status === "SUCCEEDED" && consulta.snapshot) {
consulta.snapshot.movements;
}
}Repetir é seguro. A mesma leitura pedida de novo — mesma conta,
escritório, processo e visão — devolve a mesma consulta (deduped: true), sem
nova retenção; por isso processLookups.create entra na política de
retentativa como uma leitura. Quando a próxima tentativa agendada está longe, a
chamada repetida tenta a fonte na hora, dentro da espera dela. O tempo
esgotado da fonte continua sem repetição automática.
A consulta se lê de graça, por 24 h. processLookups.get(lookupId) custa 0
e, depois da entrega, traz o processo por 24 h (resultExpiresAt), relido e
passado de novo pelo controle de confidencialidade: o processo que passou a
segredo de justiça depois responde 403. O retrato lido da consulta traz a
instância principal no topo e cada instância com os movimentos da última
captura dela, e sourcePayload é nulo. processLookups.list e
processLookups.cancel completam a superfície; o cancelamento devolve a
retenção e não gera evento.
Por webhook, sem esperar nada. O fim sai em PROCESS_LOOKUP_COMPLETED, que
aponta a consulta e nunca traz o processo; o movimento novo que a entrega
trouxer chega no PROCESS_MOVEMENT_CREATED de sempre.
A cópia guardada e a atualização, uma compra só. Com staleOnFailure: true,
o 202 traz em stale a cópia guardada, com a data e a causa, por 1 crédito, e
a consulta segue buscando a versão atual. A entrega cobra só o resto do preço
(alreadyCharged diz quanto já foi): as duas juntas custam o mesmo que a
leitura direta, e a atualização que não vier não cobra mais nada.
A leitura direta fecha a consulta. A ficha ou o retrato do mesmo processo
lidos com sucesso por processes.get ou processes.snapshot fecham a consulta
aberta sem cobrar — a leitura já cobrou.
Tetos. Até 5 consultas abertas por chave e 20 por conta; além disso, a falha da fonte volta como a leitura direta a daria. A chave de teste nunca abre consulta.
Frescor: o total: 0 qualificado
Um resultado vazio não distingue sozinho "não existe" de "a fonte ainda não indexou". A API publica a data-limite do índice da fonte; o SDK a lê nos dois eixos (processo e diário):
import {
describeFreshness,
isNotYetIndexed,
readThrough,
} from "@lexnode/legal-data-sdk";
const busca = await lexnode.processes.search({ tribunalAlias, number });
if (isNotYetIndexed(busca, { since: "2026-09-15" })) {
// Não diga "não existe": diga até quando a fonte foi lida.
mostrar(describeFreshness(busca)); // "fonte lida até 10/09/2026"
}
readThrough(busca); // Date | null — null quando a resposta não declara a data-limiteVigília com vários interessados
A vigília de um processo tem duas partes: o monitor, que consulta a fonte —
um por processo na conta, e o plano limita quantos —, e a assinatura, que
recebe o aviso — uma por externalRef, sem limite, e o externalRef volta no
evento. N interessados no mesmo processo são um monitor e N assinaturas.
import { unwatchProcess, watchProcess } from "@lexnode/legal-data-sdk";
const { monitorId, monitorAlreadyExisted, subscription } = await watchProcess(
lexnode,
{
processNumber: "00012345620268260100",
tribunalAlias: "TJSP",
officeId: "usuario-4711",
externalRef: "caso-42",
},
);
// Um interessado sai; o monitor continua ativo para os demais.
await unwatchProcess(lexnode, {
externalRef: "caso-42",
officeId: "usuario-4711",
});Quando o último interessado sair, remova o monitor com
lexnode.monitors.delete(monitorId).
Limitação conhecida: uma assinatura criada antes de o processo aparecer
na fonte não recebe PROCESS_MOVEMENT_CREATED até a primeira aparição dele. O
contorno é refazer a assinatura quando chegar o MONITOR_SYNC_SUCCEEDED com
firstAppearance: true:
import { resubscribeOnFirstAppearance } from "@lexnode/legal-data-sdk";
await resubscribeOnFirstAppearance(lexnode, evento, [
{ externalRef: "caso-42", officeId: "usuario-4711" },
]);Os tipos do contrato
O pacote exporta paths e operations, gerados do contrato OpenAPI:
import type { operations, paths } from "@lexnode/legal-data-sdk";
// Uma operação inteira, pela rota e pelo método:
type SearchProcesses = paths["/v1/processes/search"]["post"];
// Ou pelo operationId — aqui, o corpo de sucesso de listTribunals:
type Tribunals =
operations["listTribunals"]["responses"][200]["content"]["application/json"]["data"];E atalhos derivados deles:
import type { Body, Data, Query } from "@lexnode/legal-data-sdk";
type CorpoDaBusca = Body<"/v1/processes/search", "post">;
type ConsultaDeProcesso = Query<"/v1/processes/{processNumber}", "get">;
type Tribunais = Data<"/v1/tribunals", "get">;A chave de teste
A chave lapi_test_ relê o que a conta já buscou com a chave de produção, mas
não consulta a fonte nem cria trabalho em segundo plano: monitor, vigília,
colheita e busca assíncrona respondem 403 SANDBOX_UPSTREAM_DISABLED. Por isso
watchProcess e o ramo async de searchProcesses só funcionam com a chave
de produção.
Referências
- Documentação: https://www.lexnode.com.br/docs
- Contrato OpenAPI vigente: https://api.lexnode.com.br/openapi.json
- Changelog da API: https://www.lexnode.com.br/changelog
A versão deste pacote acompanha o campo info.version do documento OpenAPI. A
superfície /v1 evolui de forma aditiva: campos e rotas só entram somando, e
uma mudança incompatível exige rota nova.
