PDPJ - DATALAKE de Processos

Este notebook demonstra como usar o agregador pdpj do juscraper para consultar a API DATALAKE - API Processos do PDPJ. A API e parecida com a do Jus.br: requer um JWT no header Authorization: Bearer <token>, mas expoe varios endpoints documentados (busca profunda com filtros, contagem, partes, movimentos, documentos, texto e binario).

Sumario

  1. Autenticacao
  2. Verificar se um processo existe (existe)
  3. Detalhes de um processo (cpopg)
  4. Documentos, movimentos e partes
  5. Busca profunda (pesquisa / contar)
  6. Download dos textos e binarios (download_documents)

Como obter o token JWT

  1. Acesse https://www.jus.br e faca login com gov.br.
  2. Entre em https://portaldeservicos.pdpj.jus.br/consulta.
  3. Abra a aba Network do navegador (F12) e atualize a pagina (F5).
  4. Procure a requisicao token (POST), clique nela.
  5. Na aba Resposta, copie o valor de access_token – esse e o JWT.

O token expira em algumas horas. Para uso interativo, prefira coloca-lo em variavel de ambiente (JUSCRAPER_PDPJ_TOKEN).

import os
from getpass import getpass

import juscraper as jus

pdpj = jus.scraper("pdpj", sleep_time=0.2, verbose=1)

token = os.environ.get("JUSCRAPER_PDPJ_TOKEN") or getpass("Cole o JWT do PDPJ: ")
pdpj.auth(token=token)
True

Verificar se um processo existe

O endpoint /existe aceita um CNJ (com ou sem mascara) e devolve True/False. O scraper aceita string (retorna bool) ou lista (retorna DataFrame).

pdpj.existe("10029886420194014100")
True
pdpj.existe([
    "10029886420194014100",
    "50211226520184036100",
    "00000000000000000000",  # nao existe
])
processo existe
0 10029886420194014100 True
1 50211226520184036100 True
2 00000000000000000000 True

Detalhes do processo

cpopg(id_cnj) chama GET /processos/{numeroProcesso} e devolve um DataFrame. Cada linha tem:

  • processo: CNJ pesquisado (so digitos)
  • numero_processo: CNJ formatado pela API (NNNNNNN-DD.AAAA.J.TR.OOOO)
  • sigla_tribunal, segmento_justica, nivel_sigilo, data_atualizacao
  • detalhes: dict completo da resposta (tramitacoes, classes, assuntos, partes etc.)
df = pdpj.cpopg("1002988-64.2019.4.01.4100")
df[["processo", "numero_processo", "sigla_tribunal", "segmento_justica"]]
processo numero_processo sigla_tribunal segmento_justica
0 10029886420194014100 1002988-64.2019.4.01.4100 TRF1 JUSTICA_FEDERAL
# o dict completo fica em 'detalhes' -- util para extrair campos especificos
import json

detalhes = df.iloc[0]["detalhes"]
tramitacao = detalhes["tramitacoes"][0]
print(json.dumps({
    "instancia": tramitacao.get("instancia"),
    "natureza": tramitacao.get("natureza"),
    "competencia": tramitacao.get("dadosCompetencia"),
    "tribunal": tramitacao.get("tribunal"),
}, indent=2, ensure_ascii=False))
{
  "instancia": "PRIMEIRO_GRAU",
  "natureza": "CIVEL",
  "competencia": {
    "id": 7065,
    "idLocal": 248451,
    "nome": "Cível (exceto Ambiental/Agrário)"
  },
  "tribunal": {
    "idCodex": 3,
    "sigla": "TRF1",
    "nome": "Tribunal Regional Federal da 1ª Região",
    "segmento": "JUSTICA_FEDERAL",
    "jtr": "401"
  }
}

Multiplos processos de uma vez

cpopg aceita lista. Cada CNJ vira uma (ou mais) linhas no DataFrame final, com a coluna processo ligando a entrada do usuario a saida.

processos = [
    "10029886420194014100",
    "50211226520184036100",
    "10122143420204013300",
]

df_multi = pdpj.cpopg(processos)
df_multi[["processo", "numero_processo", "sigla_tribunal", "data_atualizacao"]]
processo numero_processo sigla_tribunal data_atualizacao
0 10029886420194014100 1002988-64.2019.4.01.4100 TRF1 2026-04-28T04:11:48.852
1 50211226520184036100 5021122-65.2018.4.03.6100 TRF3 2026-04-30T06:45:26.116
2 10122143420204013300 1012214-34.2020.4.01.3300 TRF1 2026-04-27T21:33:46.760

Documentos, movimentos e partes

Para cada processo, a API expoe sub-recursos com cada uma dessas listas. O scraper achata o JSON em DataFrames.

# Lista de documentos: cada linha e um documento. id_documento e o UUID
# que identifica o documento na API (usado depois em download_documents).
docs = pdpj.documentos("10029886420194014100")
docs[["sequencia", "data_juntada", "nome", "tipo_nome", "id_documento", "arquivo_tipo"]].head()
sequencia data_juntada nome tipo_nome id_documento arquivo_tipo
0 10 2019-12-13T14:54:51.811 Ato ordinatório.html Ato ordinatório d68cccee-b756-51e5-ae87-edb296ac220f TEXT_HTML
1 8 2019-07-17T09:32:26.372 Decisão.html Decisão d5b2fe52-8747-59c8-9953-8f320971a492 TEXT_HTML
# Lista de movimentos -- omitimos `descricao` do display por LGPD
# (pode citar nome de partes). A coluna existe no DataFrame retornado.
movs = pdpj.movimentos("10029886420194014100")
movs[["sequencia", "data_hora", "tipo_nome"]].head()
sequencia data_hora tipo_nome
0 13 2020-04-22T19:28:49.17 Baixa Definitiva
1 12 2020-04-22T19:27:25.643 Documento
2 11 2020-02-12T04:21:16.861 Decurso de Prazo
3 10 2019-12-13T15:04:04.938 Expedição de documento
4 9 2019-12-13T14:54:51.831 Documento
# Lista de partes -- omitimos `nome` e `documento_numero` do display
# por LGPD; ambas as colunas existem no DataFrame retornado.
partes = pdpj.partes("10029886420194014100")
partes[["polo", "tipo_parte", "tipo_pessoa", "situacao"]].head()
polo tipo_parte tipo_pessoa situacao
0 ATIVO AUTOR FISICA ATIVO
1 PASSIVO RÉU JURIDICA ATIVO

Busca profunda (pesquisa) e contagem (contar)

O endpoint GET /processos aceita uma colecao grande de filtros. O scraper valida-os via pydantic com nomes em snake_case e converte para o camelCase aceito pela API. Filtros principais:

snake_case (juscraper) camelCase (API) Tipo / Exemplo
numero_processo numeroProcesso "1002988..."
cpf_cnpj_parte cpfCnpjParte "123.456.789-00"
nome_parte nomeParte "FULANO"
polo_parte poloParte "ATIVO" / "PASSIVO"
oab_representante oabRepresentante "123456"
id_classe idClasse "7,12728"
id_assunto_judicial idAssuntoJudicial "7,8"
id_orgao_julgador idOrgaoJulgador ["12345", "67890"] -> "12345,67890"
instancia instancia "PRIMEIRO_GRAU"
segmento_justica segmentoJustica "JUSTICA_FEDERAL"
tribunal tribunal "TRF1,TJSP" (max 5)
data_atualizacao_inicio / _fim dataHoraAtualizacaoInicio/Fim ISO datetime
data_primeiro_ajuizamento_inicio/_fim dataHoraPrimeiroAjuizamentoInicio/Fim ISO datetime
campo_ordenacao campoOrdenacao "dataHoraUltimoMovimento"
itens_por_pagina maxElementsSize 1-100, default 100

A paginacao e feita via cursor searchAfter; o scraper cuida disso e respeita paginas (int, list, range, ou None = todas).

# Quantos processos tem o numero CNJ fornecido?
pdpj.contar(numero_processo="10029886420194014100")
1
# Busca por um CNJ especifico (1 pagina, normalmente devolve 1 resultado).
df_search = pdpj.pesquisa(
    numero_processo="10029886420194014100",
    paginas=1,
)
df_search[["numero_processo", "sigla_tribunal", "data_atualizacao", "data_ultimo_movimento"]]
numero_processo sigla_tribunal data_atualizacao data_ultimo_movimento
0 1002988-64.2019.4.01.4100 TRF1 2026-04-28T04:11:48.852 2020-04-22T19:28:49.17
# Exemplo com varios filtros: processos do TRF1 atualizados em uma janela.
# Descomente para rodar -- pode demorar conforme volume.
#
df_janela = pdpj.pesquisa(
    tribunal="TRF1",
    data_atualizacao_inicio="2026-04-01T00:00:00.000",
    data_atualizacao_fim="2026-04-30T23:59:59.999",
    paginas=range(1, 4),
)
df_janela.shape
(300, 8)

Validacao de kwargs

Nomes invalidos viram TypeError com sugestao de typo (igual cjsg/cjpg):

try:
    pdpj.pesquisa(numero_porcesso="10029886420194014100")  # typo
except TypeError as exc:
    print(exc)
PdpjScraper.pesquisa() got unexpected keyword argument(s): 'numero_porcesso' (você quis dizer 'numero_processo'?)

Baixando textos e binarios dos documentos

download_documents(base_df, ...) aceita dois shapes de entrada:

  1. DataFrame de documentos(cnj) – uma linha por documento, ja com id_documento exposto.
  2. DataFrame de cpopg(cnj) – uma linha por processo. O scraper extrai a lista de documentos do campo detalhes.

Flags:

  • with_text=True (default): baixa o texto extraido pela API (UTF-8, ja limpo).
  • with_binary=False (default): baixa o arquivo bruto (HTML, PDF, imagem…).
  • max_docs_per_process=None: limite de documentos baixados por processo.
# Baixa apenas o primeiro documento do processo, com texto.
# Nao imprimimos o conteudo aqui por LGPD (documentos podem citar
# nomes de partes/advogados); apenas confirmamos que veio.
docs = pdpj.documentos("50211226520184036100")
out = pdpj.download_documents(docs, max_docs_per_process=1, with_text=True)
print("colunas:", list(out.columns))
print("tamanho do texto:", len(out.iloc[0]["texto"]), "chars")
colunas: ['processo', 'numero_processo', 'id_documento', 'id_codex', 'id_origem', 'sequencia', 'data_juntada', 'nome', 'nivel_sigilo', 'tipo_codigo', 'tipo_nome', 'arquivo_id', 'arquivo_tipo', 'arquivo_tamanho', 'arquivo_tamanho_texto', 'arquivo_paginas', '_raw', 'texto', '_raw_texto']
tamanho do texto: 23532 chars
# Tambem da pra passar o resultado de cpopg direto
df_cpopg = pdpj.cpopg("50211226520184036100")
out2 = pdpj.download_documents(df_cpopg, max_docs_per_process=2, with_text=True, with_binary=True)
out2[["processo", "id_documento", "nome", "tipo_nome"]]
processo id_documento nome tipo_nome
0 50211226520184036100 98cff81c-17e4-5b0f-8a64-039f3a1bcf80 Acórdão.html Acórdão
1 50211226520184036100 c7ba25c6-2fff-55fa-a6bb-7242dc519739 Acórdão.html Acórdão
# 'binario' contem os bytes brutos -- HTML, PDF, etc.
print(type(out2.iloc[0]["binario"]))
print(out2.iloc[0]["binario"][:60])
<class 'bytes'>
b'<div style="color: #000000; font-style: normal; font-variant'

Persistindo em disco

Estrategia simples: salvar metadados + documentos em parquet por processo, particionando por CNJ. Util quando vai baixar lotes grandes.

import os
from tqdm import tqdm

processos = [
    "10029886420194014100",
    "50211226520184036100",
    "10122143420204013300",
]

raiz = "pdpj/cpopg"
os.makedirs(raiz, exist_ok=True)

for cnj in tqdm(processos):
    base = pdpj.cpopg(cnj)
    if base.empty:
        continue
    pasta = os.path.join(raiz, cnj)
    os.makedirs(pasta, exist_ok=True)
    base.to_parquet(os.path.join(pasta, "metadata.parquet"), index=False)
    docs = pdpj.download_documents(base, with_text=True, with_binary=False)
    if not docs.empty:
        docs.to_parquet(os.path.join(pasta, "documents.parquet"), index=False)
  0%|          | 0/3 [00:00<?, ?it/s] 33%|███▎      | 1/3 [00:00<00:01,  1.25it/s] 67%|██████▋   | 2/3 [00:12<00:07,  7.24s/it]100%|██████████| 3/3 [00:26<00:00, 10.17s/it]100%|██████████| 3/3 [00:26<00:00,  8.73s/it]

Erros comuns

  • RuntimeError: Autenticacao necessaria: chame pdpj.auth(token) antes de qualquer endpoint.
  • ValueError: Token JWT expirado: gere um novo no portal (o token vive algumas horas).
  • HTTP 500 em binarios antigos: nem todos os documentos tem binario disponivel no Codex; o download_documents registra warning e segue.
  • HTTP 429: o scraper ja faz backoff exponencial. Se persistir, aumente sleep_time ao instanciar (jus.scraper("pdpj", sleep_time=1.0)).

Referencia rapida da API publica

Metodo Endpoint REST Saida
auth(token) - True (bool)
existe(cnj) GET /processos/{n}/existe bool ou DataFrame
cpopg(cnj) GET /processos/{n} DataFrame
documentos(cnj) GET /processos/{n}/documentos DataFrame
movimentos(cnj) GET /processos/{n}/movimentos DataFrame
partes(cnj) GET /processos/{n}/partes DataFrame
pesquisa(**filtros) GET /processos DataFrame
contar(**filtros) GET /processos:contar int
download_documents(df) GET /processos/{n}/documentos/{id}/{texto,binario} DataFrame

Schemas pydantic com a fonte da verdade: juscraper.aggregators.pdpj.schemas.