Guias · Por apipj (MASTER BI TECNOLOGIA LTDA) · atualizado em · 9 min de leitura · Código executado contra a API em (Node.js 18.20, Node.js 24.16, Python 3.12, jsdom 26.1)

Como preencher o cadastro pelo CNPJ: endereço automático no formulário

Num cadastro de cliente ou fornecedor, o usuário digita o CNPJ e o resto se preenche sozinho: razão social, nome fantasia, endereço completo com o código IBGE do município, telefone, e-mail e a situação na Receita Federal. Este guia monta isso com uma rota no seu back-end (Node.js ou Python, sem dependências) e um formulário em JavaScript puro. Todo o código foi executado contra a API em 13/10/2026.

Como funciona

  1. O usuário digita ou cola o CNPJ, com ou sem máscara.
  2. Ao completar 14 caracteres, o navegador confere o dígito verificador. CNPJ digitado errado para aí, sem chamada nenhuma.
  3. O navegador chama uma rota do seu back-end: GET /api/cnpj/34028316000103.
  4. O back-end consulta https://apipj.com.br/v1/cnpj/{cnpj}?formato=completo com a chave guardada na variável de ambiente APIPJ_KEY, guarda a resposta em cache e devolve só os campos do formulário.
  5. O formulário se preenche, avisa se a empresa não está ATIVA e deixa tudo editável.
navegador  --GET /api/cnpj/{cnpj}-->  seu back-end  --Bearer APIPJ_KEY-->  apipj.com.br/v1/cnpj/{cnpj}
           <--só os campos do form--               <--resposta completa--

A chave nunca vai para o navegador

A API aceita chamadas de qualquer origem (o CORS é liberado), então dá para chamá-la direto do JavaScript da página. Não faça isso: a chave ficaria no código ou no tráfego do navegador, e qualquer visitante a copia pelo DevTools e consome a cota do seu plano. Com a rota no back-end:

  • a chave fica numa variável de ambiente do servidor;
  • o cache vale para todos os usuários, não só para uma aba;
  • a página recebe só os campos do cadastro, e não sócios, CNAEs e o resto da resposta.

Campo a campo

Os nomes abaixo são os da resposta completa (formato=completo, o padrão). Valores de exemplo do CNPJ 34.028.316/0001-03, na carga de setembro de 2026:

Campo do formulárioCaminho na resposta completaExemplo e cuidados
Razão socialrazao_socialEMPRESA BRASILEIRA DE CORREIOS E TELEGRAFOS
Nome fantasianome_fantasiaCORREIOS SEDE; null quando não há
Logradouroendereco.tipo_logradouro + endereco.logradouroSETOR + SBN QUADRA 1 BLOCO A: o tipo vem separado, junte os dois
Númeroendereco.numeroS/N em endereço sem número
Complementoendereco.complementonull quando vazio
Bairroendereco.bairroASA NORTE
CEPendereco.cep70002900: 8 dígitos, sem hífen
Cidadeendereco.municipio.nomeBRASILIA: maiúsculas e sem acento, como a Receita publica
UFendereco.ufDF
Código IBGEendereco.municipio.codigo_ibge5300108: 7 dígitos. Não confunda com codigo_rfb (9701), o código do município na Receita
Telefonetelefones[]: ddd, numero[{"ddd":"61","numero":"32144316"}]; a lista pode vir vazia
E-mailemailnull quando não há
Situaçãosituacao_cadastral.descricaoATIVA; o código fica em situacao_cadastral.codigo (02)

O código IBGE é o que costuma faltar: nota fiscal eletrônica e vários cadastros fiscais pedem o município pelo código IBGE, não pelo nome. Ele só existe no formato completo. O formato simplificado (formato=simplificado) traz o endereço já formatado (logradouro com o tipo, CEP 70.002-900, telefone (61) 3214-4316), mas não tem o código IBGE; por isso este guia usa o completo.

O back-end: uma rota com cache

A rota GET /api/cnpj/:cnpj faz quatro coisas: valida o CNPJ localmente, consulta a API com a chave, traduz a resposta para os campos do formulário e guarda o resultado. O mesmo servidor entrega a página (public/cadastro.html e public/cadastro.js, mais abaixo). Escolha a linguagem:

import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';

const API = (process.env.APIPJ_BASE_URL ?? 'https://apipj.com.br').replace(/\/+$/, '');
const CHAVE = process.env.APIPJ_KEY; // só no servidor: nunca no HTML nem no JavaScript do navegador
const PORTA = Number(process.env.PORT ?? 3000);
const DIA = 24 * 60 * 60 * 1000;
const PESOS = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];

/** Formato e dígitos verificadores, numérico ou alfanumérico (valor = código ASCII - 48). */
function cnpjValido(cnpj) {
  if (!/^[A-Z0-9]{12}[0-9]{2}$/.test(cnpj) || /^0+$/.test(cnpj)) return false;
  const v = [...cnpj].map((ch) => ch.charCodeAt(0) - 48);
  const dv = (n) => {
    let soma = 0;
    for (let i = 0; i < n; i++) soma += v[i] * PESOS[i + 13 - n];
    const resto = soma % 11;
    return resto < 2 ? 0 : 11 - resto;
  };
  return v[12] === dv(12) && v[13] === dv(13);
}

/** Só o que o formulário usa; o resto da resposta (sócios, CNAEs...) não sai do servidor. */
function paraFormulario(e) {
  const end = e.endereco;
  const tel = e.telefones[0];
  return {
    cnpj: e.cnpj,
    razao_social: e.razao_social,
    nome_fantasia: e.nome_fantasia ?? '',
    logradouro: [end.tipo_logradouro, end.logradouro].filter(Boolean).join(' '),
    numero: end.numero ?? '',
    complemento: end.complemento ?? '',
    bairro: end.bairro ?? '',
    cep: (end.cep ?? '').replace(/^(\d{5})(\d{3})$/, '$1-$2'),
    cidade: end.municipio?.nome ?? '',
    uf: end.uf ?? '',
    codigo_ibge: end.municipio?.codigo_ibge ?? '',
    telefone: tel ? `(${tel.ddd}) ${tel.numero.slice(0, -4)}-${tel.numero.slice(-4)}` : '',
    email: (e.email ?? '').toLowerCase(),
    situacao: e.situacao_cadastral.descricao,
  };
}

const cache = new Map(); // cnpj -> { expira, resposta }

async function consultar(cnpj) {
  const guardado = cache.get(cnpj);
  if (guardado && guardado.expira > Date.now()) return guardado.resposta;
  const r = await fetch(`${API}/v1/cnpj/${cnpj}?formato=completo`, {
    headers: { Authorization: `Bearer ${CHAVE}` },
    signal: AbortSignal.timeout(8000),
  });
  let resposta;
  if (r.status === 200) resposta = { status: 200, corpo: paraFormulario(await r.json()) };
  else if (r.status === 404) resposta = { status: 404, corpo: { erro: 'CNPJ não encontrado na base da Receita Federal' } };
  else if (r.status === 429) {
    return { status: 429, corpo: { erro: 'Limite de consultas atingido; tente de novo em instantes' }, retryAfter: r.headers.get('retry-after') };
  } else if (r.status === 400) return { status: 400, corpo: { erro: 'CNPJ inválido' } };
  else throw new Error(`apipj respondeu HTTP ${r.status}`); // 401 (chave), 5xx...
  // Encontrado: 30 dias (a base é mensal). Não encontrado: 1 dia, para não segurar uma empresa recém-aberta.
  if (cache.size >= 10_000) cache.delete(cache.keys().next().value);
  cache.set(cnpj, { expira: Date.now() + (resposta.status === 200 ? 30 * DIA : DIA), resposta });
  return resposta;
}

function json(res, status, corpo, extras = {}) {
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', ...extras });
  res.end(JSON.stringify(corpo));
}

const ESTATICOS = {
  '/': ['cadastro.html', 'text/html; charset=utf-8'],
  '/cadastro.js': ['cadastro.js', 'text/javascript; charset=utf-8'],
};

createServer(async (req, res) => {
  const { pathname } = new URL(req.url, 'http://localhost');
  const rota = pathname.match(/^\/api\/cnpj\/(.*)$/);
  if (req.method === 'GET' && rota) {
    const cnpj = rota[1].replace(/[.-]/g, '').toUpperCase();
    if (!cnpjValido(cnpj)) return json(res, 400, { erro: 'CNPJ inválido' }); // nem chega à API
    try {
      const r = await consultar(cnpj);
      return json(res, r.status, r.corpo, r.retryAfter ? { 'Retry-After': r.retryAfter } : {});
    } catch (e) {
      console.error('apipj:', e.message);
      return json(res, 502, { erro: 'Consulta de CNPJ indisponível' });
    }
  }
  const estatico = ESTATICOS[pathname];
  if (req.method === 'GET' && estatico) {
    res.writeHead(200, { 'Content-Type': estatico[1] });
    return res.end(await readFile(new URL(`./public/${estatico[0]}`, import.meta.url)));
  }
  json(res, 404, { erro: 'Rota não encontrada' });
}).listen(PORTA, () => console.log(`Formulário em http://localhost:${PORTA}`));
import json
import os
import re
import threading
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

API = os.environ.get("APIPJ_BASE_URL", "https://apipj.com.br").rstrip("/")
CHAVE = os.environ["APIPJ_KEY"]  # só no servidor: nunca no HTML nem no JavaScript do navegador
PORTA = int(os.environ.get("PORT", "8000"))
PUBLICO = Path(__file__).parent / "public"
DIA = 24 * 60 * 60
PESOS = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
FORMATO = re.compile(r"[A-Z0-9]{12}[0-9]{2}")


def cnpj_valido(cnpj):
    """Formato e dígitos verificadores, numérico ou alfanumérico (valor = código ASCII - 48)."""
    if not FORMATO.fullmatch(cnpj) or cnpj == "0" * 14:
        return False
    v = [ord(ch) - 48 for ch in cnpj]

    def dv(n):
        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 para_formulario(e):
    """Só o que o formulário usa; o resto da resposta (sócios, CNAEs...) não sai do servidor."""
    end = e["endereco"]
    municipio = end.get("municipio") or {}
    tel = e["telefones"][0] if e["telefones"] else None
    cep = end.get("cep") or ""
    return {
        "cnpj": e["cnpj"],
        "razao_social": e["razao_social"],
        "nome_fantasia": e.get("nome_fantasia") or "",
        "logradouro": " ".join(p for p in (end.get("tipo_logradouro"), end.get("logradouro")) if p),
        "numero": end.get("numero") or "",
        "complemento": end.get("complemento") or "",
        "bairro": end.get("bairro") or "",
        "cep": f"{cep[:5]}-{cep[5:]}" if len(cep) == 8 else cep,
        "cidade": municipio.get("nome") or "",
        "uf": end.get("uf") or "",
        "codigo_ibge": municipio.get("codigo_ibge") or "",
        "telefone": f"({tel['ddd']}) {tel['numero'][:-4]}-{tel['numero'][-4:]}" if tel else "",
        "email": (e.get("email") or "").lower(),
        "situacao": e["situacao_cadastral"]["descricao"],
    }


_cache = {}  # cnpj -> (expira, resposta)
_trava = threading.Lock()


def consultar(cnpj):
    with _trava:
        guardado = _cache.get(cnpj)
    if guardado and guardado[0] > time.time():
        return guardado[1]
    pedido = urllib.request.Request(
        f"{API}/v1/cnpj/{cnpj}?formato=completo", headers={"Authorization": f"Bearer {CHAVE}"}
    )
    try:
        with urllib.request.urlopen(pedido, timeout=8) as r:
            resposta = (200, para_formulario(json.load(r)), None)
    except urllib.error.HTTPError as e:
        if e.code == 404:
            resposta = (404, {"erro": "CNPJ não encontrado na base da Receita Federal"}, None)
        elif e.code == 429:
            return (429, {"erro": "Limite de consultas atingido; tente de novo em instantes"}, e.headers.get("Retry-After"))
        elif e.code == 400:
            return (400, {"erro": "CNPJ inválido"}, None)
        else:
            raise  # 401 (chave), 5xx...
    # Encontrado: 30 dias (a base é mensal). Não encontrado: 1 dia, para não segurar uma empresa recém-aberta.
    with _trava:
        if len(_cache) >= 10_000:
            _cache.pop(next(iter(_cache)))
        _cache[cnpj] = (time.time() + (30 * DIA if resposta[0] == 200 else DIA), resposta)
    return resposta


ESTATICOS = {
    "/": ("cadastro.html", "text/html; charset=utf-8"),
    "/cadastro.js": ("cadastro.js", "text/javascript; charset=utf-8"),
}


class Rotas(BaseHTTPRequestHandler):
    def do_GET(self):
        caminho = self.path.split("?")[0]
        if caminho.startswith("/api/cnpj/"):
            cnpj = re.sub(r"[.-]", "", caminho[len("/api/cnpj/"):]).upper()
            if not cnpj_valido(cnpj):
                return self.enviar(400, {"erro": "CNPJ inválido"})  # nem chega à API
            try:
                status, corpo, retry_after = consultar(cnpj)
            except Exception as e:
                self.log_error("apipj: %s", e)
                return self.enviar(502, {"erro": "Consulta de CNPJ indisponível"})
            return self.enviar(status, corpo, retry_after)
        if caminho in ESTATICOS:
            arquivo, tipo = ESTATICOS[caminho]
            return self.enviar(200, (PUBLICO / arquivo).read_bytes(), tipo=tipo)
        self.enviar(404, {"erro": "Rota não encontrada"})

    def enviar(self, status, corpo, retry_after=None, tipo="application/json; charset=utf-8"):
        dados = corpo if isinstance(corpo, bytes) else json.dumps(corpo, ensure_ascii=False).encode()
        self.send_response(status)
        self.send_header("Content-Type", tipo)
        self.send_header("Content-Length", str(len(dados)))
        if retry_after:
            self.send_header("Retry-After", retry_after)
        self.end_headers()
        self.wfile.write(dados)


if __name__ == "__main__":
    print(f"Formulário em http://localhost:{PORTA}")
    ThreadingHTTPServer(("", PORTA), Rotas).serve_forever()

Para rodar, com a chave na variável de ambiente (nunca no código):

export APIPJ_KEY="apipj_live_sua_chave"
node servidor.mjs      # ou: python servidor.py

O que a rota devolve ao navegador:

SituaçãoHTTP da sua rotaCorpo
Encontrado200Os 14 campos do formulário
CNPJ mal formado ou com dígito errado400{"erro":"CNPJ inválido"}, sem chamar a API
CNPJ válido, fora da base da Receita404{"erro":"CNPJ não encontrado na base da Receita Federal"}
Limite por minuto ou cota do mês429Repassa o Retry-After da API, quando ele vem
Chave errada, API fora do ar, tempo esgotado502{"erro":"Consulta de CNPJ indisponível"}; o motivo vai para o log do servidor

Detalhes que importam:

  • Validação local antes de tudo. O dígito verificador é conferido no servidor também (o navegador não é confiável). CNPJ errado não gasta cota, e o código aceita o CNPJ alfanumérico: cada caractere vale o código ASCII menos 48, com pesos 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2 e módulo 11.
  • Cache de 30 dias para quem existe, 1 dia para o 404. A base é mensal, então repetir a consulta no mesmo mês só gasta cota. O 404 também conta na cota, mas fica pouco tempo guardado para não esconder uma empresa que entre na carga seguinte.
  • 429 não é cacheado. É um estado passageiro: o Retry-After vai para o navegador, que mostra quanto esperar.
  • APIPJ_BASE_URL existe só para apontar a rota para uma API falsa nos testes (foi assim que os caminhos 429 e 502 foram exercitados).
  • Cache em memória serve para um processo. Com várias instâncias, troque o Map (ou o dict) por Redis ou por uma tabela, com o mesmo prazo.

Testando a rota com curl, contra a API real, em 13/10/2026:

for c in 34028316000103 11378117000121 99999999000191; do
  curl -s -w "  -> HTTP %{http_code}
" localhost:3000/api/cnpj/$c
done
{"cnpj":"34028316000103","razao_social":"EMPRESA BRASILEIRA DE CORREIOS E TELEGRAFOS","nome_fantasia":"CORREIOS SEDE","logradouro":"SETOR SBN QUADRA 1 BLOCO A","numero":"S/N","complemento":"","bairro":"ASA NORTE","cep":"70002-900","cidade":"BRASILIA","uf":"DF","codigo_ibge":"5300108","telefone":"(61) 3214-4316","email":"acgtescnpj@correios.com.br","situacao":"ATIVA"}  -> HTTP 200
{"erro":"CNPJ inválido"}  -> HTTP 400
{"erro":"CNPJ não encontrado na base da Receita Federal"}  -> HTTP 404

O segundo tem o último dígito errado (o certo é 11378117000120) e nem chega à API. O terceiro é válido, mas não existe na base da Receita. A versão em Python devolve os mesmos campos e os mesmos códigos.

O front-end: formulário em JavaScript puro

O HTML é um formulário comum. O que faz diferença: o aviso fica num elemento com role="status" e aria-live="polite", ligado ao campo pelo aria-describedby, para que leitores de tela anunciem "preenchido", "não encontrado" ou o alerta de situação; os campos têm autocomplete; e o campo do CNPJ não usa inputmode="numeric", porque o CNPJ alfanumérico tem letras.

<!doctype html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Cadastro de cliente</title>
</head>
<body>
  <form id="cadastro" method="post" action="/clientes">
    <label for="cnpj">CNPJ</label>
    <input id="cnpj" name="cnpj" required maxlength="18" autocomplete="off" spellcheck="false"
           autocapitalize="characters" aria-describedby="cnpj-aviso">
    <p id="cnpj-aviso" role="status" aria-live="polite"></p>

    <label for="razao_social">Razão social</label>
    <input id="razao_social" name="razao_social" required autocomplete="organization">
    <label for="nome_fantasia">Nome fantasia</label>
    <input id="nome_fantasia" name="nome_fantasia">
    <label for="situacao">Situação na Receita Federal</label>
    <input id="situacao" name="situacao" readonly>

    <label for="cep">CEP</label>
    <input id="cep" name="cep" autocomplete="postal-code" inputmode="numeric">
    <label for="logradouro">Logradouro</label>
    <input id="logradouro" name="logradouro" autocomplete="address-line1">
    <label for="numero">Número</label>
    <input id="numero" name="numero">
    <label for="complemento">Complemento</label>
    <input id="complemento" name="complemento" autocomplete="address-line2">
    <label for="bairro">Bairro</label>
    <input id="bairro" name="bairro">
    <label for="cidade">Cidade</label>
    <input id="cidade" name="cidade" autocomplete="address-level2">
    <label for="uf">UF</label>
    <input id="uf" name="uf" maxlength="2" autocomplete="address-level1">
    <input type="hidden" name="codigo_ibge">

    <label for="telefone">Telefone</label>
    <input id="telefone" name="telefone" type="tel" autocomplete="tel">
    <label for="email">E-mail</label>
    <input id="email" name="email" type="email" autocomplete="email">

    <button type="submit">Salvar</button>
  </form>
  <script src="/cadastro.js" defer></script>
</body>
</html>

O script valida o dígito verificador, espera o usuário parar de digitar, consulta a rota e preenche os campos:

const PESOS = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
function cnpjValido(cnpj) {
  if (!/^[A-Z0-9]{12}[0-9]{2}$/.test(cnpj) || /^0+$/.test(cnpj)) return false;
  const v = [...cnpj].map((ch) => ch.charCodeAt(0) - 48);
  const dv = (n) => {
    let soma = 0;
    for (let i = 0; i < n; i++) soma += v[i] * PESOS[i + 13 - n];
    const resto = soma % 11;
    return resto < 2 ? 0 : 11 - resto;
  };
  return v[12] === dv(12) && v[13] === dv(13);
}

const CAMPOS = ['razao_social', 'nome_fantasia', 'situacao', 'cep', 'logradouro', 'numero', 'complemento',
  'bairro', 'cidade', 'uf', 'codigo_ibge', 'telefone', 'email'];
const FALHOU = 'Não foi possível buscar os dados agora. Preencha à mão; o cadastro funciona do mesmo jeito.';
const form = document.getElementById('cadastro');
const campoCnpj = form.elements.cnpj;
const aviso = document.getElementById('cnpj-aviso');
const respostas = new Map(); // cnpj -> dados, ou null (não encontrado): o mesmo CNPJ não é consultado duas vezes
let ultimo = '';
let preenchidoCom = '';
let espera;

const normalizar = (valor) => valor.replace(/[^0-9A-Za-z]/g, '').toUpperCase();
const avisar = (texto, tipo = 'info') => { aviso.textContent = texto; aviso.dataset.tipo = tipo; };

function limpar() {
  for (const nome of CAMPOS) form.elements[nome].value = '';
  preenchidoCom = '';
}

function mostrar(cnpj, dados) {
  if (!dados) {
    if (preenchidoCom) limpar();
    return avisar('CNPJ não encontrado na base da Receita Federal. Preencha os dados à mão.', 'erro');
  }
  for (const nome of CAMPOS) form.elements[nome].value = dados[nome] ?? '';
  preenchidoCom = cnpj;
  if (dados.situacao !== 'ATIVA') avisar(`Atenção: a situação deste CNPJ na Receita Federal é ${dados.situacao}.`, 'alerta');
  else avisar(`Dados de ${dados.razao_social} preenchidos. Confira antes de salvar.`);
}

async function buscar(cnpj) {
  if (respostas.has(cnpj)) return mostrar(cnpj, respostas.get(cnpj));
  if (preenchidoCom) limpar(); // tira os dados de outro CNPJ
  avisar('Buscando os dados do CNPJ…');
  form.setAttribute('aria-busy', 'true');
  try {
    const r = await fetch(`/api/cnpj/${cnpj}`);
    if (r.ok || r.status === 404) {
      respostas.set(cnpj, r.ok ? await r.json() : null);
      if (normalizar(campoCnpj.value) === cnpj) mostrar(cnpj, respostas.get(cnpj)); // senão, já digitaram outro
      return;
    }
    ultimo = ''; // erro passageiro: deixa tentar de novo
    if (r.status === 400) avisar('CNPJ inválido. Confira os dígitos.', 'erro');
    else if (r.status === 429) avisar(`Muitas consultas seguidas. Tente de novo em ${r.headers.get('Retry-After') || 60} segundos ou preencha à mão.`, 'erro');
    else avisar(FALHOU, 'erro');
  } catch {
    ultimo = '';
    avisar(FALHOU, 'erro');
  } finally {
    form.removeAttribute('aria-busy');
  }
}

campoCnpj.addEventListener('input', () => {
  clearTimeout(espera);
  const cnpj = normalizar(campoCnpj.value);
  if (cnpj.length < 14) { ultimo = ''; return avisar(''); }
  if (cnpj === ultimo) return;
  if (!cnpjValido(cnpj)) return avisar('CNPJ inválido. Confira os dígitos.', 'erro');
  espera = setTimeout(() => { ultimo = cnpj; buscar(cnpj); }, 400); // debounce: espera o usuário parar de digitar
});

Como ele se comporta:

  • Debounce de 400 ms. Digitando caractere a caractere, sai uma chamada só, quando o CNPJ fica completo.
  • O mesmo CNPJ não é consultado duas vezes. O Map guarda as respostas da página, inclusive o "não encontrado"; voltar a um CNPJ já visto preenche sem rede.
  • Resposta atrasada não sobrescreve. Se o usuário já trocou o CNPJ quando a resposta chega, ela é guardada e não preenche a tela.
  • Situação diferente de ATIVA vira alerta, como Atenção: a situação deste CNPJ na Receita Federal é BAIXADA. O cadastro não é bloqueado: decidir se aceita uma empresa baixada ou inapta é regra do seu negócio.
  • Falha não trava o cadastro. Em 429, 502 ou erro de rede, o aviso pede para preencher à mão e os campos continuam editáveis. Um novo input no campo tenta de novo.
  • Funciona com máscara e com minúsculas: 34.028.316/0001-03 e 00.000.000/e08g-12 viram 34028316000103 e 00000000E08G12 antes da validação.

O teste automatizado abriu a página servida pelo próprio back-end (Node.js e Python) no jsdom, digitou os CNPJs e conferiu os campos. Com 34.028.316/0001-03, os 13 campos ficaram assim (o codigo_ibge vai num campo oculto):

razao_social  EMPRESA BRASILEIRA DE CORREIOS E TELEGRAFOS    cep          70002-900
nome_fantasia CORREIOS SEDE                                  logradouro   SETOR SBN QUADRA 1 BLOCO A
situacao      ATIVA                                          numero       S/N
bairro        ASA NORTE                                      complemento  (vazio)
cidade        BRASILIA                                       uf           DF
codigo_ibge   5300108                                        telefone     (61) 3214-4316
email         acgtescnpj@correios.com.br

E os avisos, na ordem do teste:

34.028.316/0001-03  Dados de EMPRESA BRASILEIRA DE CORREIOS E TELEGRAFOS preenchidos. Confira antes de salvar.
33066408000115      Atenção: a situação deste CNPJ na Receita Federal é BAIXADA.
99999999000191      CNPJ não encontrado na base da Receita Federal. Preencha os dados à mão.
11378117000121      CNPJ inválido. Confira os dígitos.
00.000.000/e08g-12  Dados de BANCO DO BRASIL SA preenchidos. Confira antes de salvar.

Boas práticas

  • Cache de até 30 dias. Os dados abertos do CNPJ mudam uma vez por mês; o campo atualizado_em da resposta diz de qual carga eles vêm. Um cache de 30 dias no back-end corta a maior parte das consultas repetidas.
  • Respeite o limite por minuto do plano. O gratuito tem 10 consultas por minuto e 50.000 por mês; o Dev, R$ 12,90 por mês, tem 30 por minuto. Um formulário digitado por pessoas raramente chega perto, mas uma importação em lote chega: nesse caso, controle o ritmo no servidor.
  • Proteja a sua rota. Sem proteção, /api/cnpj/ vira uma consulta de CNPJ grátis para qualquer um, paga pela sua cota. Exija sessão de usuário logado ou ponha um limite por IP.
  • Não bloqueie o cadastro se a API falhar. O preenchimento é conveniência: tempo esgotado, 429 ou 502 devem deixar o usuário digitar à mão e salvar.
  • Peça confirmação. O endereço da Receita é o do estabelecimento naquele CNPJ, que pode não ser o de entrega ou de cobrança. Deixe os campos editáveis.
  • LGPD: guarde só o que o cadastro usa. A resposta completa traz o quadro de sócios, com nomes e CPF mascarado; a rota deste guia nem repassa esses dados ao navegador. Em empresário individual e MEI, a razão social costuma ser o nome da pessoa e o endereço pode ser a casa dela: são dados pessoais, mesmo vindos de uma base pública. Salve os campos do formulário, não o JSON inteiro, com a data da consulta se precisar provar a origem.

Perguntas frequentes

Posso chamar a API direto do navegador?

Tecnicamente sim, o CORS é liberado. Mas a chave ficaria exposta para qualquer visitante. Do navegador, chame só GET /v1/valida/{cnpj}, que é aberto, não pede chave e não consome cota, se quiser validar o CNPJ sem escrever o algoritmo. A consulta dos dados fica no back-end.

Dá para preencher só pelo CEP?

O CEP sozinho não identifica a empresa; o que este guia preenche vem do CNPJ. Se o usuário corrigir o CEP depois, valide com o serviço de CEP que você já usa.

Funciona com o CNPJ alfanumérico?

Sim. A validação do dígito nos três trechos de código (Node.js, Python e navegador) aceita letras nas 12 primeiras posições, e a consulta é a mesma. O teste usou 00000000E08G12, o primeiro CNPJ alfanumérico emitido pela Receita. O guia do CNPJ alfanumérico tem as regras.

Quantas consultas um formulário gasta?

Uma por CNPJ distinto por mês, com o cache do back-end. CNPJ com dígito errado não gasta nada, porque é barrado antes. O CNPJ válido que não existe na base gasta uma consulta, que o cache segura por um dia.

Próximos passos