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
- 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. - Guarde a chave numa variável de ambiente, fora do código:
export APIPJ_KEY="apipj_live_sua_chave"
pip install requestsO 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
Sessionpara 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 oRetry-Aftere 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-91Erros que você vai encontrar
| HTTP | code | O que fazer |
|---|---|---|
| 200 | Use o documento. | |
| 400 | invalid_cnpj | O CNPJ tem formato ou dígito errado. Valide com cnpj_valido antes e mostre a mensagem ao usuário. |
| 401 | unauthorized | Chave ausente, errada ou revogada. Confira APIPJ_KEY. |
| 404 | not_found | O CNPJ não está na base. O cliente devolve None. |
| 429 | rate_limited | Passou do limite por minuto. O cliente espera o Retry-After e repete. |
| 429 | quota_exceeded | Acabou a cota do mês. O cliente lança ApipjErro; troque de plano ou espere a virada do mês. |
| 5xx | Instabilidade. 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_cnpjConsultas 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 empresaEm 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
- Documentação completa: todos os campos, formatos e cabeçalhos.
- CNPJ alfanumérico: regras e o cálculo do dígito explicado.
- Em outra linguagem: JavaScript, PHP, C#.