npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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. fetch global e node: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ê — uma baseUrl sem 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çalho X-Office-Id.

Instalação

npm install @lexnode/legal-data-sdk

O 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 trouxe

O 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.cause diz por que a fonte não entregou: UPSTREAM_TIMEOUT, UPSTREAM_UNAVAILABLE ou UPSTREAM_RATE_LIMITED;
  • meta.lakeRescue.retryAfterSeconds, repetido no header Retry-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.retryAfterSeconds fica null no sucesso;
  • meta.sourceFetchedAt diz 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 consulta SUCCEEDED sem consulta aberta (lookupId nulo) que traz o processo em process (visão PROCESS) ou snapshot (visão SNAPSHOT), com as instâncias do número em instances, o mesmo meta e 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 em creditsReserved, 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-limite

Vigí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

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.