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

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

  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. 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 o Retry-After em segundos e repete; quota_exceeded (cota do mês) lança erro na hora.
  • Concorrência pela fila. consultarVarios cria 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-91

O 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;
  }
}
HTTPcodeComportamento do módulo
200Devolve o documento.
400invalid_cnpjLança ApipjErro. Valide antes com cnpjValido.
401unauthorizedLança ApipjErro. Confira APIPJ_KEY.
404not_foundDevolve null.
429rate_limitedEspera o Retry-After e repete.
429quota_exceededLança ApipjErro.
5xxRepete 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