Exemplo

Busca de empresas paginada por cursor

Busca de empresas brasileiras por UF, município, CNAE, natureza jurídica e situação na API do cnpj.ia.br, percorrendo as páginas pelo meta.next_cursor em Python e curl.

Python curl Código no GitHub

A busca é um GET /v1/cnpjs com filtros; cada resposta traz até 20 empresas e um meta.next_cursor. A página seguinte é a mesma chamada, com os mesmos filtros, mais cursor. O exemplo percorre as páginas até next_cursor vir nulo ou até um teto de páginas, e soma o que cada página cobrou.

O problema

Montar uma lista de empresas por filtro (as ativas de um CNAE num município, os órgãos federais de uma UF) passa de uma página. Paginação por número de página falha quando a base muda no meio; o cursor não. O erro comum é trocar um filtro entre uma página e outra, o que invalida o cursor, ou seguir paginando sem teto e pagar por empresas que ninguém vai usar.

O fluxo

  1. Chama GET /v1/cnpjs com os filtros. O padrão do exemplo é uf=DF&natureza=1015&situacao=ATIVA: órgãos do Poder Executivo Federal ativos no DF.
  2. Processa data (até 20 empresas no perfil basic) e guarda meta.next_cursor.
  3. Repete com os mesmos filtros mais cursor, até next_cursor vir nulo ou até --max-paginas.
  4. A contagem do filtro vem na primeira página, em meta.total_count_capped, com teto de exibição.

A busca exige plano pago: no Free, responde insufficient_plan. Os filtros aceitos estão em Busca de empresas.

O código

Python, com requests (python busca_paginada.py --filtro uf=SC --filtro situacao=ATIVA --max-paginas 3):

"""Busca empresas por filtros e percorre as páginas pelo cursor.

    CNPJIA_KEY=... python busca_paginada.py
    CNPJIA_KEY=... python busca_paginada.py --filtro uf=SC --filtro situacao=ATIVA --max-paginas 3

Cada página traz até 20 empresas no perfil basic. A próxima página é a mesma
chamada, com os MESMOS filtros, mais cursor=<meta.next_cursor>; mudar um filtro
invalida o cursor. meta.next_cursor null = última página. A busca exige plano
pago (no Free responde insufficient_plan) e cobra por empresa retornada: veja
https://cnpj.ia.br/precos.
"""

import argparse
import os
import sys
import time

import requests

API = "https://api.cnpj.ia.br"
ESPERA_MAXIMA = 60

# Órgãos do Poder Executivo Federal ativos no DF (natureza jurídica 1015).
FILTROS_PADRAO = {"uf": "DF", "natureza": "1015", "situacao": "ATIVA"}


def get(sessao, url, params):
    """GET com timeout; em 429/503 espera o Retry-After e repete uma vez."""
    for tentativa in (1, 2):
        resp = sessao.get(url, params=params, timeout=(5, 30))
        if tentativa == 1 and resp.status_code in (429, 503):
            espera = resp.headers.get("Retry-After", "")
            if espera.isdigit() and int(espera) <= ESPERA_MAXIMA:
                print(f"HTTP {resp.status_code}: aguardando {espera}s (Retry-After)", file=sys.stderr)
                time.sleep(int(espera))
                continue
        return resp


def paginas(sessao, filtros, max_paginas):
    """Gera uma página (o JSON inteiro) por chamada, seguindo meta.next_cursor."""
    cursor = None
    for _ in range(max_paginas):
        params = dict(filtros, cursor=cursor) if cursor else filtros
        resp = get(sessao, f"{API}/v1/cnpjs", params)
        corpo = resp.json()
        if not resp.ok:
            erro = corpo.get("error", {})
            sys.exit(
                f"HTTP {resp.status_code}: {erro.get('code')}: {erro.get('message')} "
                f"(request_id {erro.get('request_id')})"
            )
        yield corpo
        cursor = corpo["meta"].get("next_cursor")
        if not cursor:
            return


def main():
    parser = argparse.ArgumentParser(description="Busca paginada na API do cnpj.ia.br.")
    parser.add_argument("--filtro", action="append", metavar="CAMPO=VALOR", help="repita para vários filtros")
    parser.add_argument("--max-paginas", type=int, default=2)
    args = parser.parse_args()

    chave = os.environ.get("CNPJIA_KEY")
    if not chave:
        print("Defina CNPJIA_KEY.", file=sys.stderr)
        sys.exit(2)

    filtros = dict(f.split("=", 1) for f in args.filtro) if args.filtro else FILTROS_PADRAO

    sessao = requests.Session()
    sessao.headers["Authorization"] = f"Bearer {chave}"

    n_paginas = n_empresas = creditos = 0
    total = "?"
    for pagina in paginas(sessao, filtros, args.max_paginas):
        n_paginas += 1
        for empresa in pagina["data"]:
            n_empresas += 1
            print(f"{empresa['cnpj']};{empresa['razao_social']}")
        creditos += pagina["meta"]["credits_charged"]
        if n_paginas == 1:  # a contagem (com teto de exibição) vem na primeira página
            total = pagina["meta"].get("total_count_capped") or "?"

    print(
        f"# páginas: {n_paginas}, empresas: {n_empresas}, total do filtro: {total}, créditos cobrados: {creditos}",
        file=sys.stderr,
    )


if __name__ == "__main__":
    main()

A versão em curl, com jq lendo o cursor, está em curl/busca-paginada.sh. A busca cobra por empresa retornada, e página vazia não cobra; a tabela abaixo conta mil empresas retornadas, o equivalente a 50 páginas cheias. O valor vigente está em Preços.

Custo em créditos

OperaçãoQuantidade por mil registrosPeso em créditosCréditos
Busca, empresas retornadas100011000
Total por mil registros1000

Atualizado em