Guias · atualizado em · 3 min de leitura · Equipe apipj

Como consultar CNPJ em Python

Este guia monta um cliente Python pequeno para a API de CNPJ da apipj: consulta um CNPJ, valida o dígito verificador antes de gastar cota (inclusive o CNPJ alfanumérico), trata "não encontrado" e limite por minuto, consulta em lote e guarda em cache. O código foi executado contra a API em 09/10/2026 com Python 3.12 e requests 2.32.

Antes de começar

  1. Crie uma conta grátis e copie a chave (apipj_live_...). O plano gratuito tem 10 consultas por minuto e 50.000 por mês.
  2. Guarde a chave numa variável de ambiente, fora do código:
export APIPJ_KEY="apipj_live_sua_chave"
pip install requests

O cliente

Salve como apipj_cnpj.py:

import os
import re
import time
from concurrent.futures import ThreadPoolExecutor

import requests

API_BASE = os.environ.get("APIPJ_BASE_URL", "https://apipj.com.br").rstrip("/")
API_KEY = os.environ["APIPJ_KEY"]  # nunca deixe a chave no código-fonte

_sessao = requests.Session()  # reaproveita a conexão TLS entre consultas
_sessao.headers.update({"Authorization": f"Bearer {API_KEY}", "User-Agent": "minha-app/1.0"})


class ApipjErro(Exception):
    def __init__(self, status: int, code: str, message: str):
        super().__init__(f"{status} {code}: {message}")
        self.status = status
        self.code = code


def normalizar(cnpj: str) -> str:
    """Tira pontuação e passa para maiúsculas (o CNPJ alfanumérico tem letras)."""
    return re.sub(r"[^0-9A-Za-z]", "", cnpj).upper()


PESOS = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]


def cnpj_valido(cnpj: str) -> bool:
    """Formato e dígitos verificadores, numérico ou alfanumérico (valor = código ASCII - 48)."""
    c = normalizar(cnpj)
    if not re.fullmatch(r"[A-Z0-9]{12}[0-9]{2}", c):
        return False
    v = [ord(ch) - 48 for ch in c]

    def dv(n: int) -> int:  # n = 12 (primeiro DV) ou 13 (segundo DV)
        resto = sum(v[i] * PESOS[i + 13 - n] for i in range(n)) % 11
        return 0 if resto < 2 else 11 - resto

    return v[12] == dv(12) and v[13] == dv(13)


def consultar_cnpj(cnpj: str, formato: str = "completo", tentativas: int = 3) -> dict | None:
    """Documento do CNPJ, ou None se ele não existe na base da Receita."""
    c = normalizar(cnpj)
    for tentativa in range(1, tentativas + 1):
        r = _sessao.get(f"{API_BASE}/v1/cnpj/{c}", params={"formato": formato}, timeout=10)
        if r.status_code == 200:
            return r.json()
        if r.status_code == 404:
            return None
        corpo = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
        code = corpo.get("code", "erro")
        # 429 por minuto: espere o Retry-After e repita. Cota do mês esgotada (quota_exceeded) não adianta repetir.
        if r.status_code == 429 and code == "rate_limited" and tentativa < tentativas:
            time.sleep(float(r.headers.get("Retry-After", "1")))
            continue
        if r.status_code >= 500 and tentativa < tentativas:
            time.sleep(2 ** tentativa)
            continue
        raise ApipjErro(r.status_code, code, corpo.get("message", r.text[:200]))
    raise ApipjErro(429, "rate_limited", "limite por minuto atingido em todas as tentativas")


def consultar_varios(cnpjs: list[str], simultaneas: int = 5) -> dict[str, dict | None]:
    """Consulta em paralelo, com poucas conexões ao mesmo tempo (o limite por minuto vale para a chave)."""
    unicos = list(dict.fromkeys(normalizar(c) for c in cnpjs if cnpj_valido(c)))
    with ThreadPoolExecutor(max_workers=simultaneas) as pool:
        return dict(zip(unicos, pool.map(consultar_cnpj, unicos)))

Três decisões que valem para qualquer integração:

  • Uma Session para todas as consultas. Reaproveitar a conexão evita refazer o TLS a cada chamada. Medido a partir do Brasil: mediana de 28 ms por consulta reaproveitando a conexão, contra 72 ms abrindo uma nova a cada consulta.
  • 404 vira None. CNPJ que não existe, ou que é novo demais para a carga do mês, é um resultado normal, não uma exceção.
  • Dois tipos de 429. rate_limited é o limite por minuto: espere o Retry-After e repita. quota_exceeded é a cota do mês: repetir só desperdiça tempo.

Primeira consulta

from apipj_cnpj import consultar_cnpj

empresa = consultar_cnpj("11.378.117/0001-20")
print(empresa["razao_social"])                              # LEADS2B S/A
print(empresa["situacao_cadastral"]["descricao"])           # ATIVA
print(empresa["endereco"]["municipio"]["codigo_ibge"])      # 4106902
print(empresa["cnae_principal"]["descricao"])

Prefere campos já formatados para exibir na tela? Use o formato simplificado:

simples = consultar_cnpj("00000000000191", formato="simplificado")
print(simples["cnpj"])   # 00.000.000/0001-91

Erros que você vai encontrar

HTTPcodeO que fazer
200Use o documento.
400invalid_cnpjO CNPJ tem formato ou dígito errado. Valide com cnpj_valido antes e mostre a mensagem ao usuário.
401unauthorizedChave ausente, errada ou revogada. Confira APIPJ_KEY.
404not_foundO CNPJ não está na base. O cliente devolve None.
429rate_limitedPassou do limite por minuto. O cliente espera o Retry-After e repete.
429quota_exceededAcabou a cota do mês. O cliente lança ApipjErro; troque de plano ou espere a virada do mês.
5xxInstabilidade. O cliente repete com espera crescente (2 s, 4 s).
from apipj_cnpj import ApipjErro, consultar_cnpj

try:
    consultar_cnpj("11378117000121")   # dígito errado
except ApipjErro as e:
    print(e.status, e.code)            # 400 invalid_cnpj

Consultas em lote

consultar_varios remove inválidos e duplicados antes de chamar a API e faz até 5 consultas ao mesmo tempo:

from apipj_cnpj import consultar_varios

resultado = consultar_varios(["11378117000120", "00000000000191", "60746948000112", "invalido"])
for cnpj, empresa in resultado.items():
    print(cnpj, empresa["razao_social"] if empresa else "não encontrado")

Mais conexões simultâneas não aumentam o seu limite por minuto: ajuste simultaneas para caber nele. No plano gratuito, 10 por minuto, uma lista grande leva tempo de propósito; quando passar do limite, o cliente espera sozinho. Os planos pagos (Preços) vão de 30 a 200 consultas por minuto.

Cache: a forma mais barata de consultar menos

A base da Receita muda uma vez por mês, então guardar a resposta por até 30 dias é seguro para a maioria dos cadastros:

import json, time
from pathlib import Path
from apipj_cnpj import consultar_cnpj, normalizar

CACHE = Path("cache-cnpj")
CACHE.mkdir(exist_ok=True)
VALIDADE = 30 * 24 * 3600

def consultar_com_cache(cnpj: str) -> dict | None:
    arquivo = CACHE / f"{normalizar(cnpj)}.json"
    if arquivo.exists() and time.time() - arquivo.stat().st_mtime < VALIDADE:
        return json.loads(arquivo.read_text(encoding="utf-8"))
    empresa = consultar_cnpj(cnpj)
    if empresa is not None:
        arquivo.write_text(json.dumps(empresa, ensure_ascii=False), encoding="utf-8")
    return empresa

Em produção, troque o arquivo por Redis ou por uma tabela no seu banco; a lógica é a mesma.

Do JSON para o seu cadastro

Um mapeamento típico para um formulário de cliente:

def para_cadastro(e: dict) -> dict:
    end = e["endereco"]
    return {
        "cnpj": e["cnpj_formatado"],
        "razao_social": e["razao_social"],
        "nome_fantasia": e["nome_fantasia"] or "",
        "situacao": e["situacao_cadastral"]["descricao"],
        "cnae": e["cnae_principal"]["codigo"],
        "logradouro": " ".join(filter(None, [end["tipo_logradouro"], end["logradouro"]])),
        "numero": end["numero"] or "S/N",
        "bairro": end["bairro"] or "",
        "cep": end["cep"],
        "cidade": end["municipio"]["nome"],
        "uf": end["uf"],
        "codigo_ibge": end["municipio"]["codigo_ibge"],
        "optante_simples": bool(e["simples"] and e["simples"]["optante"]),
    }

Bloqueie o cadastro quando situacao não for ATIVA, ou ao menos avise o usuário: empresa baixada ou inapta é a causa mais comum de nota fiscal recusada.

Próximos passos