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==
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:
- Nunca ordene por
dataAjuizamentoesperando ordem cronológica. Um processo compacto de 1998 vence um processo ISO de 2025. - Nunca filtre um período em
dataAjuizamentosem 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.