Pular para o conteúdo

A API Pública do DataJud

O que é o DataJud

O DataJud é a base nacional de dados processuais do CNJ, criada pela Resolução CNJ nº 331/2020.

O funcionamento é o seguinte: todo tribunal do país é obrigado a enviar ao CNJ os dados dos seus processos, num formato padronizado (o MNI). O CNJ recebe, indexa tudo num Elasticsearch e publica duas coisas — os painéis de estatística que você já conhece, e uma API pública que devolve os processos um a um.

É essa API que vamos usar.

A URL e a chave

Cada tribunal tem seu próprio endereço. O do TJRJ é:

https://api-publica.datajud.cnj.jus.br/api_publica_tjrj/_search

Troque tjrj pela sigla de outro tribunal e você consulta o acervo dele — api_publica_tjsp, api_publica_trf2, e assim por diante.

A autenticação é um cabeçalho HTTP:

Authorization: APIKey cDZHYzlZa0JadVREZDJCendQbXY6SkJlTzNjLV9TRENyQk1RdnFKZGRQdw==
Nota

Essa chave é pública. O próprio CNJ a publica na documentação da API, e ela é a mesma para todo mundo. Ela não identifica você, não tem cota nominal e não abre nada além do que já é público.

Ainda assim, no código do curso ela é lida de uma variável de ambiente. Não por paranoia com esta chave em particular, mas porque o dia em que você usar uma credencial que seja secreta — a do banco do Tribunal, por exemplo — o código não vai precisar mudar. Só o valor.

O cliente

Todo o acesso à API cabe numa função. É o arquivo exemplos/cnj.py:

import json
import os
import urllib.error
import urllib.request

TRIBUNAL = os.environ.get("DATAJUD_TRIBUNAL", "tjrj")
URL = f"https://api-publica.datajud.cnj.jus.br/api_publica_{TRIBUNAL}/_search"

CHAVE = os.environ.get(
    "DATAJUD_API_KEY",
    "cDZHYzlZa0JadVREZDJCendQbXY6SkJlTzNjLV9TRENyQk1RdnFKZGRQdw==",
)


class ErroDataJud(Exception):
    """Falha na conversa com a API. Carrega o texto que o CNJ devolveu."""


def consultar(corpo: dict, timeout: int = 60) -> dict:
    """Envia uma consulta à API Pública do DataJud e devolve a resposta."""
    requisicao = urllib.request.Request(
        URL,
        data=json.dumps(corpo).encode("utf-8"),
        headers={
            "Authorization": f"APIKey {CHAVE}",
            "Content-Type": "application/json",
        },
        method="POST",
    )

    try:
        with urllib.request.urlopen(requisicao, timeout=timeout) as resposta:
            return json.load(resposta)
    except urllib.error.HTTPError as e:
        # O corpo do erro do Elasticsearch diz exatamente o que está errado
        # na sua consulta. Jogar essa mensagem fora é jogar fora o diagnóstico.
        detalhe = e.read().decode("utf-8", "replace")
        raise ErroDataJud(f"HTTP {e.code}: {detalhe[:500]}") from None
    except urllib.error.URLError as e:
        raise ErroDataJud(
            f"Não foi possível alcançar a API do CNJ ({e.reason}). "
            "Verifique sua conexão e se a rede do Tribunal permite a saída."
        ) from None

Repare em duas escolhas.

Nenhuma dependência. urllib já vem com o Python. Você não precisa instalar requests para falar com o CNJ, e num computador corporativo com pip restrito isso não é um detalhe.

O erro carrega o corpo da resposta. Quando sua consulta está malformada, o Elasticsearch responde HTTP 400 e explica no corpo qual campo está errado. Um except que engolisse essa mensagem transformaria um diagnóstico preciso em "deu erro". Você vai usar isso no próximo capítulo — e o agente também.

A primeira chamada

Este é o equivalente judicial daquele get_weather do curso original: uma função que chama uma API e imprime o que voltou.

import json
import sys

from cnj import consultar

# No Windows, o terminal não fala UTF-8 por padrão. Sem esta linha, {: #no-windows-o-terminal-não-fala-utf-8-por-padrão-sem-esta-linha }
# "Distribuição" vira "Distribui??o" na tela. O dado está certo; {: #distribuição-vira-distribuio-na-tela-o-dado-está-certo }
# quem está errado é o terminal. {: #quem-está-errado-é-o-terminal }
sys.stdout.reconfigure(encoding="utf-8")

resposta = consultar({"size": 1, "query": {"match_all": {}}})

print("Total de processos do TJRJ no DataJud:", resposta["hits"]["total"])
print()

processo = resposta["hits"]["hits"][0]["_source"]

print("Campos disponíveis:", ", ".join(sorted(processo)))
print()

processo["movimentos"] = processo["movimentos"][:2]
print(json.dumps(processo, ensure_ascii=False, indent=2))

Rode:

python exemplos\primeira_chamada.py

E o que sai:

Total de processos do TJRJ no DataJud: {'value': 10000, 'relation': 'gte'}

Campos disponíveis: @timestamp, assuntos, classe, dataAjuizamento,
dataHoraUltimaAtualizacao, formato, grau, id, movimentos, nivelSigilo,
numeroProcesso, orgaoJulgador, sistema, tribunal

{
  "id": "TJRJ_JE_00071533920248190002",
  "tribunal": "TJRJ",
  "grau": "JE",
  "numeroProcesso": "00071533920248190002",
  "dataAjuizamento": "20240802145113",
  "nivelSigilo": 0,
  "orgaoJulgador": {
    "codigo": 7697,
    "nome": "NITEROI I JUI ESP CRIM",
    "codigoMunicipioIBGE": 3303302
  },
  "classe": {
    "codigo": 278,
    "nome": "Termo Circunstanciado"
  },
  "sistema": {
    "codigo": -1,
    "nome": "Inválido"
  },
  "formato": {
    "codigo": 1,
    "nome": "Eletrônico"
  },
  "dataHoraUltimaAtualizacao": "2026-08-11T06:33:28.939000Z",
  "@timestamp": "2026-08-11T06:33:28.939000Z",
  "movimentos": [
    {
      "codigo": 26,
      "dataHora": "2024-08-02T14:51:13.000Z",
      "nome": "Distribuição",
      "complementosTabelados": [
        {
          "codigo": 2,
          "descricao": "tipo_de_distribuicao_redistribuicao",
          "valor": 1,
          "nome": "competência exclusiva"
        }
      ],
      "orgaoJulgador": {
        "codigo": "7697",
        "nome": "NITEROI I JUI ESP CRIM"
      }
    },
    {
      "codigo": 85,
      "dataHora": "2024-08-02T18:55:16.000Z",
      "nome": "Petição",
      "orgaoJulgador": {"codigo": "7697", "nome": "NITEROI I JUI ESP CRIM"}
    }
  ],
  "assuntos": [
    {
      "codigo": 3692,
      "nome": "Contravenções Penais"
    }
  ]
}

Um Termo Circunstanciado do Juizado Especial Criminal de Niterói, distribuído em 2 de agosto de 2024. É um processo que existe.

Cinco coisas que esse retorno já ensina

1. O total mente — de propósito

{'value': 10000, 'relation': 'gte'}

Não são dez mil processos. relation: gte significa "dez mil ou mais": o Elasticsearch para de contar em 10.000 porque contar o resto seria caro e, para quem só quer a primeira página, inútil.

Se você precisar do número verdadeiro, é preciso pedir explicitamente:

{"size": 0, "track_total_hits": True, "query": {"match_all": {}}}

Guarde essa armadilha. Um agente que lê value: 10000 e responde "encontrei dez mil processos" está errado, e está errado com uma confiança que engana.

2. O número do processo vem sem máscara

"numeroProcesso": "00071533920248190002"

Vinte dígitos crus. O formato que um juiz lê é 0007153-39.2024.8.19.0002.

A conversão é posicional e você já conhece o formato pela Unidade 1 — NNNNNNN-DD.AAAA.J.TR.OOOO. Vamos escrevê-la como parte das Tools, porque devolver ao magistrado uma tira de vinte dígitos é entregar trabalho manual junto com a resposta.

3. Existem dois formatos de data no mesmo documento

dataAjuizamento, neste documento, é "20240802145113" — uma string compacta, AAAAMMDDHHMMSS. Guarde o "neste documento": o item 3b mostra que não vale para todos.

movimentos[].dataHora é "2024-08-02T14:51:13.000Z" — ISO 8601.

Não é inconsistência do CNJ por descuido: são campos com origens diferentes no MNI. Para você importa que o mesmo instante tem duas grafias, e que a comparação entre eles exige conversão.

E há um degrau a mais, que só aparece medindo:

3b. E os dois formatos convivem dentro do mesmo campo

dataAjuizamento não é sempre compacto. No índice do TJRJ, o mesmo campo aparece nas duas grafias, dependendo do processo:

"dataAjuizamento": "20260617161919"             ← compacto
"dataAjuizamento": "2020-12-29T00:00:00.000Z"   ← ISO

Isso tem uma consequência que não é cosmética. Quando o Elasticsearch lê um campo de data cujo valor só tem dígitos, ele interpreta o número como milissegundos desde 1970:

20260617161919  →  como data:         2026-06-17
                →  como epoch_millis: 2612-01-13

Toda data compacta é lançada para o século 27, e portanto acima de qualquer data ISO na ordenação. Medido no assunto Violência Doméstica: 73,2% dos registros estão na grafia compacta, e um date_histogram por ano devolve baldes de 2608 a 2612.

Duas regras práticas saem daqui, e as duas custaram código nesta unidade:

  1. Nunca ordene por dataAjuizamento esperando ordem cronológica. Um processo compacto de 1998 vence um processo ISO de 2025.
  2. Nunca filtre um período em dataAjuizamento sem conferir quantos registros ficaram de fora. O filtro "óbvio" descarta uma família inteira, e a série que sobra parece limpa.

A Unidade 3 dedica um capítulo a esta armadilha, porque ela é o exemplo mais nítido do curso de uma consulta certa produzindo uma resposta errada.

4. sistema.nome pode ser "Inválido"

"sistema": {"codigo": -1, "nome": "Inválido"}

Este é o primeiro dado sujo que você encontra, e ele apareceu no primeiro processo que a API devolveu. Não é um caso de borda raro.

Dado real de tribunal tem buracos. Uma Tool que assume que todo campo está preenchido vai quebrar — e vai quebrar dentro do laço do agente, onde o diagnóstico é mais difícil.

5. nivelSigilo: 0

Aparece em todo documento. É o assunto do capítulo Sigilo na origem, e a resposta curta é: todos eles são 0.

Fixando o capítulo

Q1: A API devolveu "total": {"value": 10000, "relation": "gte"}. Quantos processos atendem à consulta?

Q2: Por que o cliente consultar() inclui o corpo da resposta na mensagem de erro, em vez de só o código HTTP?

Q3: A primeira resposta da API trouxe "sistema": {"codigo": -1, "nome": "Inválido"}. Qual é a lição de projeto?