Como consultar CNPJ em JavaScript (Node.js)
Este guia monta um módulo JavaScript sem dependências para a API de CNPJ da apipj, usando o fetch que já vem no Node.js 18 em diante. Ele valida o dígito verificador localmente (inclusive o CNPJ alfanumérico), trata "não encontrado" e limite por minuto e consulta listas com concorrência controlada. O código foi executado contra a API em 09/10/2026 no Node.js 18.20 e 24.16.
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. - Deixe a chave numa variável de ambiente do seu servidor:
export APIPJ_KEY="apipj_live_sua_chave"Do servidor, nunca do navegador
A chave de API dá acesso à sua cota. Se ela for para o JavaScript do front-end, qualquer visitante a copia pelo DevTools e consome o seu plano. O caminho certo é o navegador chamar uma rota do seu back-end, que chama a apipj com a chave guardada no servidor. É também no back-end que o cache funciona para todos os usuários.
O módulo
Salve como apipj-cnpj.mjs:
const API_BASE = (process.env.APIPJ_BASE_URL ?? 'https://apipj.com.br').replace(/\/+$/, '');
const API_KEY = process.env.APIPJ_KEY; // nunca deixe a chave no código-fonte nem no front-end
export class ApipjErro extends Error {
constructor(status, code, message) {
super(`${status} ${code}: ${message}`);
this.status = status;
this.code = code;
}
}
const espera = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export const normalizar = (cnpj) => String(cnpj).replace(/[^0-9A-Za-z]/g, '').toUpperCase();
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). */
export function cnpjValido(cnpj) {
const c = normalizar(cnpj);
if (!/^[A-Z0-9]{12}[0-9]{2}$/.test(c)) return false;
const v = [...c].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);
}
/** Documento do CNPJ, ou null se ele não existe na base da Receita. */
export async function consultarCnpj(cnpj, { formato = 'completo', tentativas = 3 } = {}) {
const c = normalizar(cnpj);
for (let t = 1; t <= tentativas; t++) {
const r = await fetch(`${API_BASE}/v1/cnpj/${c}?formato=${formato}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
signal: AbortSignal.timeout(10_000),
});
if (r.status === 200) return r.json();
if (r.status === 404) return null;
const corpo = await r.json().catch(() => ({}));
// 429 por minuto: espere o Retry-After e repita. Cota do mês esgotada (quota_exceeded) não adianta repetir.
if (r.status === 429 && corpo.code === 'rate_limited' && t < tentativas) {
await espera(Number(r.headers.get('retry-after') ?? 1) * 1000);
continue;
}
if (r.status >= 500 && t < tentativas) {
await espera(2 ** t * 1000);
continue;
}
throw new ApipjErro(r.status, corpo.code ?? 'erro', corpo.message ?? r.statusText);
}
throw new ApipjErro(429, 'rate_limited', 'limite por minuto atingido em todas as tentativas');
}
/** Consulta em paralelo, com poucas requisições ao mesmo tempo (o limite por minuto vale para a chave). */
export async function consultarVarios(cnpjs, simultaneas = 5) {
const fila = [...new Set(cnpjs.filter(cnpjValido).map(normalizar))];
const resultados = new Map();
const trabalhador = async () => {
while (fila.length) {
const c = fila.shift();
resultados.set(c, await consultarCnpj(c));
}
};
await Promise.all(Array.from({ length: simultaneas }, trabalhador));
return resultados;
}Escolhas que importam:
AbortSignal.timeout(10_000)corta uma requisição presa. Sem ele, um problema de rede deixa a promessa pendurada para sempre.- 404 vira
null. CNPJ inexistente, ou novo demais para a carga do mês, é resultado, não exceção. - Dois tipos de 429.
rate_limited(limite por minuto) espera oRetry-Afterem segundos e repete;quota_exceeded(cota do mês) lança erro na hora. - Concorrência pela fila.
consultarVarioscria N trabalhadores que puxam da mesma fila: nunca há mais de N requisições ao mesmo tempo, sem biblioteca extra.
Primeira consulta
import { consultarCnpj } from './apipj-cnpj.mjs';
const empresa = await consultarCnpj('11.378.117/0001-20');
console.log(empresa.razao_social); // LEADS2B S/A
console.log(empresa.situacao_cadastral.descricao); // ATIVA
console.log(empresa.endereco.municipio.codigo_ibge); // 4106902
const simples = await consultarCnpj('00000000000191', { formato: 'simplificado' });
console.log(simples.cnpj); // 00.000.000/0001-91O await no topo funciona em módulos ES (.mjs ou "type": "module" no package.json).
Tratando erros
import { ApipjErro, consultarCnpj } from './apipj-cnpj.mjs';
try {
await consultarCnpj('11378117000121'); // dígito errado
} catch (e) {
if (e instanceof ApipjErro && e.code === 'invalid_cnpj') {
// mostre ao usuário: "CNPJ inválido"
} else if (e instanceof ApipjErro && e.code === 'quota_exceeded') {
// cota do mês acabou: alerte o time, troque de plano
} else {
throw e;
}
}| HTTP | code | Comportamento do módulo |
|---|---|---|
| 200 | Devolve o documento. | |
| 400 | invalid_cnpj | Lança ApipjErro. Valide antes com cnpjValido. |
| 401 | unauthorized | Lança ApipjErro. Confira APIPJ_KEY. |
| 404 | not_found | Devolve null. |
| 429 | rate_limited | Espera o Retry-After e repete. |
| 429 | quota_exceeded | Lança ApipjErro. |
| 5xx | Repete com espera de 2 s e 4 s. |
Lista de CNPJs
import { consultarVarios } from './apipj-cnpj.mjs';
const resultado = await consultarVarios(['11378117000120', '00000000000191', '60746948000112', 'invalido']);
for (const [cnpj, empresa] of resultado) {
console.log(cnpj, empresa ? empresa.razao_social : 'não encontrado');
}Entradas inválidas e repetidas saem antes de qualquer chamada, então não gastam cota. Aumentar simultaneas não aumenta o limite da chave: no plano gratuito (10 por minuto) uma lista longa leva o tempo do limite, e o módulo espera sozinho quando recebe 429. Os planos pagos vão de 30 a 200 por minuto (Preços).
Cache
A base da Receita é atualizada uma vez por mês. Guarde cada resposta por até 30 dias: num Map em memória para um processo só, ou em Redis quando houver várias instâncias. Cache no back-end serve a todos os usuários e é o que mais reduz consultas.
Próximos passos
- Documentação completa: campos, formatos e cabeçalhos
X-RateLimit-*eX-Quota-*. - CNPJ alfanumérico: regras e cálculo do dígito explicado.
- Em outra linguagem: Python, PHP, C#.