Consulta de CNPJ com Node.js e TypeScript: valide fornecedores PJ

Capa do artigo Consulta de CNPJ com Node.js e TypeScript: valide fornecedores PJ

A consulta de CNPJ com Node.js resolve um problema que todo cadastro de fornecedor ou cliente PJ tem: saber se a empresa existe, se está ativa na Receita Federal e se o que ela declarou (razão social, atividade, porte) bate com o que o seu negócio exige. Com TypeScript e o fetch nativo do Node 20, isso cabe em um módulo pequeno, tipado e fácil de testar.

O tutorial cobre a cadeia inteira: validar os dígitos localmente para não gastar consulta com número errado, chamar a API de CNPJ da APIBrasil, tipar a resposta, aplicar regras de negócio (situação cadastral, CNAE, porte, Simples e MEI) e guardar o resultado em cache para não pagar duas vezes pela mesma empresa.

Os nomes de campos e o endpoint foram conferidos na documentação em doc.apibrasil.io. Onde a resposta pode variar, o código trata o campo como opcional e o texto diz o que confirmar.

Validar x consultar CNPJ

Validar e consultar são operações diferentes, e confundir as duas custa dinheiro. Validar é verificar, sem sair do seu servidor, se os 14 dígitos formam um CNPJ possível: tamanho, dígitos verificadores e sequências repetidas como 00000000000000. Consultar é perguntar à base da Receita o que existe por trás daquele número.

A validação local é gratuita e instantânea; a consulta é cobrada por chamada. Então a ordem é sempre a mesma: valida, e só consulta o que passou. Isso elimina erros de digitação, colagens com caracteres estranhos e tentativas de cadastro com número inventado antes que eles virem custo.

O algoritmo dos dígitos verificadores é o mesmo em qualquer linguagem; o Programador Brasil tem um guia para validar CPF e CNPJ em JavaScript, PHP e Python com a explicação dos pesos. Aqui a função entra pronta, em TypeScript, como primeira barreira do fluxo.

Setup: TypeScript e fetch nativo

O projeto usa Node.js 20 ou superior, que já traz fetch, e o tsx para executar TypeScript sem etapa de build durante o desenvolvimento. Nenhuma biblioteca HTTP é necessária.

mkdir consulta-cnpj && cd consulta-cnpj
npm init -y
npm install -D typescript tsx @types/node
npm pkg set type=module
npx tsc --init --target es2022 --module nodenext --moduleResolution nodenext --strict

# .env (fora do git)
APIBRASIL_TOKEN=seu_bearer_token

A consulta de CNPJ com Node.js é um POST em /dados/cnpj/credits com o campo tipo igual a “cnpj” e o número em cnpj. Com homolog: true, a resposta vem de uma base fixa de testes, sem cobrança, o que é ideal para rodar a suíte de testes sem consumir créditos.

// src/cnpj.ts
import type { RespostaCnpj } from './tipos.js';

const BASE_URL = 'https://gateway.apibrasil.io/api/v2';

export function somenteDigitos(valor: string): string {
  return valor.replace(/\D/g, '');
}

export function cnpjValido(valor: string): boolean {
  const cnpj = somenteDigitos(valor);
  if (cnpj.length !== 14 || /^(\d)\1{13}$/.test(cnpj)) return false;
  const calc = (base: string, pesos: number[]) => {
    const soma = base.split('').reduce((acc, d, i) => acc + Number(d) * pesos[i], 0);
    const resto = soma % 11;
    return resto < 2 ? 0 : 11 - resto;
  };
  const p1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
  const p2 = [6, ...p1];
  const d1 = calc(cnpj.slice(0, 12), p1);
  const d2 = calc(cnpj.slice(0, 12) + d1, p2);
  return cnpj.endsWith(`${d1}${d2}`);
}

export async function consultarCnpj(valor: string, homolog = false): Promise<RespostaCnpj> {
  const cnpj = somenteDigitos(valor);
  if (!cnpjValido(cnpj)) throw new Error(`CNPJ inválido: ${valor}`);

  const resposta = await fetch(`${BASE_URL}/dados/cnpj/credits`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.APIBRASIL_TOKEN}`,
    },
    body: JSON.stringify({ tipo: 'cnpj', cnpj, homolog }),
    signal: AbortSignal.timeout(15_000),
  });

  if (resposta.status === 402) throw new Error('Saldo insuficiente na APIBrasil');
  if (resposta.status >= 500) throw new Error(`HTTP ${resposta.status}`); // transitório: pode repetir
  const corpo = (await resposta.json()) as RespostaCnpj;
  // O erro pode vir com HTTP 200: confira sempre o campo error.
  if (!resposta.ok || corpo.error) throw new Error(corpo.message ?? `HTTP ${resposta.status}`);
  return corpo;
}

Dois detalhes do fetch nativo: AbortSignal.timeout evita que uma consulta presa segure o seu processo, e o status 402 é tratado antes do parse porque indica saldo insuficiente, um caso em que repetir não adianta. O tipo RespostaCnpj vem a seguir.

Tipando a resposta da API

A resposta tem um envelope comum a todas as APIs da plataforma (error, message, balance, tax, status_code) e, dentro de data, o resultado da consulta. No exemplo da documentação, os dados cadastrais aparecem em data.cnpj e são repetidos em data.dados; o código lê o primeiro e cai no segundo se precisar. Os campos abaixo são os que a documentação mostra.

// src/tipos.ts
export interface Envelope<T> {
  error: boolean;
  message: string;
  status_code: number;
  balance?: string;   // string em formato brasileiro, ex.: "107,620"
  tax?: string;
  homolog?: boolean;
  data?: T;
}

export interface DadosCnpj {
  cnpj: string;
  nome_fantasia?: string;
  situacao_cadastral: string;          // código: "02" = ativa
  data_situacao_cadastral?: string;    // AAAAMMDD
  motivo_situacao?: { codigo: string; descricao: string };
  data_inicio_atividades?: string;     // AAAAMMDD
  cnae_fiscal: string;                 // ex.: "6311900"
  cnae_fiscal_secundaria?: string;     // lista separada por vírgula
  cnae?: Array<{ codigo: string; descricao: string }>;
  uf?: string;
  municipio?: { codigo: string; descricao: string };
  correio_eletronico?: string;
  ddd1?: string;
  telefone1?: string;
  empresa?: {
    razao_social: string;
    natureza_juridica?: string;
    porte_empresa?: string;            // "01" ME, "03" EPP, "05" demais
    capital_social?: number | string;  // número no exemplo; trate os dois
  };
  simples?: {
    opcao_simples?: 'S' | 'N';
    opcao_mei?: 'S' | 'N';
    data_opcao_simples?: string;
    data_exclusao_simples?: string;
  };
  socios?: Array<{
    nome_socio: string;
    cnpj_cpf_socio?: string;           // vem mascarado
    qualificacao?: { codigo: string; descricao: string };
    data_entrada_sociedade?: string;
  }>;
}

export interface ResultadoCnpj {
  error: boolean;
  status: boolean;
  msg?: string;
  cnpj?: DadosCnpj;
  dados?: DadosCnpj;
}

export type RespostaCnpj = Envelope<ResultadoCnpj>;

export function extrairDados(r: RespostaCnpj): DadosCnpj {
  const d = r.data?.cnpj ?? r.data?.dados;
  if (!d) throw new Error(r.data?.msg ?? 'Resposta sem dados cadastrais');
  return d;
}

Três escolhas de tipagem merecem justificativa. A situação cadastral é string porque a Receita a publica como código de dois dígitos, não como texto. capital_social aceita número ou string porque a documentação mostra os dois formatos em endpoints diferentes da mesma API. E quase tudo é opcional: a API agrega bases que nem sempre têm todos os campos, e um tipo rígido demais quebra em produção no primeiro CNPJ fora do padrão.

Infográfico com o fluxo de validação de fornecedor PJ via consulta de CNPJ com Node.js: validar dígitos, consultar a Receita pela API, checar situação ativa, conferir CNAE e porte e guardar em cache por 24 horas

Regras de negócio: situação cadastral, CNAE, porte e Simples

Com os dados tipados, a validação de fornecedor vira uma lista de regras que devolve aprovado ou não, com os motivos. Cada regra é uma decisão do seu negócio; as abaixo são um ponto de partida comum em compras e cadastro.

CampoValoresRegra típica
situacao_cadastral01 nula, 02 ativa, 03 suspensa, 04 inapta, 08 baixadaSó aceitar 02
cnae_fiscal e cnae_fiscal_secundariaCódigos de 7 dígitosExigir ao menos um CNAE compatível com o serviço contratado
empresa.porte_empresa01 ME, 03 EPP, 05 demaisAlçada de aprovação por porte
empresa.capital_socialValor em reaisMínimo por categoria de contrato
simples.opcao_simples / opcao_meiS ou NDefine retenção de impostos e tipo de nota fiscal
data_inicio_atividadesAAAAMMDDTempo mínimo de operação
// src/regras.ts
import type { DadosCnpj } from './tipos.js';

export interface Avaliacao {
  aprovado: boolean;
  motivos: string[];
}

export interface Politica {
  cnaesPermitidos: string[];     // prefixos, ex.: "62" cobre toda a divisão de TI
  capitalMinimo?: number;
  mesesMinimosAtividade?: number;
  aceitaMei?: boolean;
}

function mesesDesde(aaaammdd?: string): number {
  if (!aaaammdd || aaaammdd.length !== 8) return 0;
  const inicio = new Date(+aaaammdd.slice(0, 4), +aaaammdd.slice(4, 6) - 1, +aaaammdd.slice(6, 8));
  return (Date.now() - inicio.getTime()) / (1000 * 60 * 60 * 24 * 30);
}

export function avaliarFornecedor(d: DadosCnpj, p: Politica): Avaliacao {
  const motivos: string[] = [];

  if (d.situacao_cadastral !== '02') {
    motivos.push(`Situação cadastral ${d.situacao_cadastral} (${d.motivo_situacao?.descricao ?? 'sem motivo'})`);
  }

  const cnaes = [d.cnae_fiscal, ...(d.cnae_fiscal_secundaria?.split(',') ?? [])].filter(Boolean);
  if (!cnaes.some((c) => p.cnaesPermitidos.some((prefixo) => c.startsWith(prefixo)))) {
    motivos.push(`Nenhum CNAE compatível: ${cnaes.join(', ')}`);
  }

  const capital = Number(String(d.empresa?.capital_social ?? 0).replace(',', '.'));
  if (p.capitalMinimo && capital < p.capitalMinimo) motivos.push(`Capital social abaixo do mínimo: ${capital}`);

  if (p.mesesMinimosAtividade && mesesDesde(d.data_inicio_atividades) < p.mesesMinimosAtividade) {
    motivos.push('Empresa com menos tempo de atividade do que o exigido');
  }

  if (p.aceitaMei === false && d.simples?.opcao_mei === 'S') motivos.push('MEI não aceito para este contrato');

  return { aprovado: motivos.length === 0, motivos };
}

Uma regra que não aparece na API, mas pesa no cadastro, é a prova de contato: a Receita informa o telefone cadastrado, porém quem preenche o formulário pode ser qualquer pessoa. Para um cadastro PJ confiável, vale verificar o telefone no cadastro com Node.js por código de confirmação, fechando o ciclo entre empresa real e pessoa real.

Cache e limite de consultas

Cada consulta é cobrada e a base da Receita muda devagar, então o cache é a otimização com melhor retorno. Guardar o resultado por 24 horas cobre o caso mais comum, o mesmo CNPJ sendo consultado várias vezes no mesmo dia (cadastro, aprovação, emissão de nota), sem risco relevante de dado defasado.

Três regras completam o cache. Primeiro, deduplique chamadas simultâneas: se dois requests pedem o mesmo CNPJ no mesmo segundo, só um deve ir à API. Segundo, não repita respostas 4xx; elas indicam erro de entrada, token ou saldo. Terceiro, repita apenas timeouts e 5xx, com espera crescente e poucas tentativas.

// src/cache.ts — cache em memória com deduplicação de chamadas em andamento
import { consultarCnpj, somenteDigitos } from './cnpj.js';
import { extrairDados } from './tipos.js';
import type { DadosCnpj } from './tipos.js';

const TTL_MS = 24 * 60 * 60 * 1000;
const cache = new Map<string, { dados: DadosCnpj; expiraEm: number }>();
const emAndamento = new Map<string, Promise<DadosCnpj>>();

async function comRetry<T>(fn: () => Promise<T>, tentativas = 3): Promise<T> {
  for (let i = 0; ; i++) {
    try {
      return await fn();
    } catch (erro) {
      const msg = String(erro);
      const transitorio = msg.includes('HTTP 5') || msg.includes('TimeoutError');
      if (!transitorio || i === tentativas - 1) throw erro;
      await new Promise((r) => setTimeout(r, 500 * 2 ** i));
    }
  }
}

export async function obterCnpj(valor: string): Promise<DadosCnpj> {
  const cnpj = somenteDigitos(valor);
  const hit = cache.get(cnpj);
  if (hit && hit.expiraEm > Date.now()) return hit.dados;

  const pendente = emAndamento.get(cnpj);
  if (pendente) return pendente;

  const promessa = comRetry(() => consultarCnpj(cnpj, process.env.APIBRASIL_HOMOLOG === 'true'))
    .then((r) => {
      const dados = extrairDados(r);
      cache.set(cnpj, { dados, expiraEm: Date.now() + TTL_MS });
      return dados;
    })
    .finally(() => emAndamento.delete(cnpj));

  emAndamento.set(cnpj, promessa);
  return promessa;
}

Em produção com mais de uma instância, troque o Map por Redis com o mesmo TTL; a lógica de deduplicação continua válida por processo. Se o volume for alto, acompanhe o campo balance das respostas e dispare um alerta antes do saldo zerar, porque o 402 chega sem aviso.

Para lotes grandes, como higienizar uma planilha com milhares de fornecedores, limite a concorrência a poucas chamadas simultâneas e processe em ordem, em vez de disparar tudo com Promise.all. Além de respeitar o limite mensal do plano, isso mantém o consumo de saldo previsível e facilita retomar de onde parou se o processo cair no meio.

Exemplo completo: consulta de CNPJ com Node.js em um endpoint

Para fechar, um script de linha de comando que recebe o CNPJ, roda a validação, consulta com cache e aplica a política. O mesmo obterCnpj serve dentro de um handler HTTP (Express, Fastify ou Next.js) sem alteração.

// src/validar.ts — uso: npx tsx src/validar.ts 44.959.669/0001-80
import { cnpjValido } from './cnpj.js';
import { obterCnpj } from './cache.js';
import { avaliarFornecedor } from './regras.js';

const entrada = process.argv[2];
if (!entrada || !cnpjValido(entrada)) {
  console.error('Informe um CNPJ válido (14 dígitos).');
  process.exit(1);
}

const politica = { cnaesPermitidos: ['62', '63'], capitalMinimo: 5000, mesesMinimosAtividade: 12, aceitaMei: true };

try {
  const dados = await obterCnpj(entrada);
  const avaliacao = avaliarFornecedor(dados, politica);
  console.log(`${dados.empresa?.razao_social ?? dados.cnpj} — ${dados.municipio?.descricao}/${dados.uf}`);
  console.log(avaliacao.aprovado ? 'APROVADO' : `REPROVADO:\n- ${avaliacao.motivos.join('\n- ')}`);
} catch (erro) {
  console.error('Falha na consulta:', (erro as Error).message);
  process.exit(2);
}

Rode com APIBRASIL_HOMOLOG=true nas primeiras execuções para validar o fluxo sem custo. Depois, troque para a base real e compare a saída com o cartão CNPJ da própria Receita para um ou dois casos conhecidos; esse teste manual pega erros de leitura de campo antes que eles cheguem ao cadastro.

Perguntas frequentes

A consulta de CNPJ é gratuita na APIBrasil?

A consulta real é cobrada por chamada, com o valor descontado do saldo e informado no campo tax de cada resposta. Para desenvolvimento e testes automatizados, use homolog: true, que responde de uma base fixa sem cobrança. O preço atual está no catálogo em doc.apibrasil.io e no painel.

O que significa situação cadastral 02, 03, 04 ou 08?

São os códigos da Receita Federal: 02 é ativa, 03 suspensa, 04 inapta e 08 baixada (01 é nula). Para fornecedores e clientes PJ, o normal é aceitar apenas 02 e registrar o motivo retornado em motivo_situacao para os demais, porque uma empresa inapta pode regularizar e voltar.

Posso enviar o CNPJ com pontos e barra?

O exemplo da documentação usa o formato com máscara, mas o código deste guia remove tudo que não é dígito antes de enviar e de usar como chave de cache. Assim o mesmo CNPJ digitado de formas diferentes não gera duas consultas nem duas entradas no cache.

Como consultar os sócios de uma empresa?

A resposta da consulta por CNPJ já traz a lista em socios, com nome, qualificação e data de entrada, e o CPF vem mascarado. A mesma API tem o tipo lista-socios para quando você precisa só dessa parte; os campos estão descritos na página da API de CNPJ.

Com que frequência devo atualizar o cadastro?

Depende do risco: para pagamento a fornecedores, reconsultar na véspera de cada pagamento é prática comum; para clientes, uma revalidação mensal costuma bastar. O cache de 24 horas deste guia resolve a repetição no mesmo dia, e a política de atualização é uma regra do seu processo.

Próximos passos

Com validação, consulta de CNPJ com Node.js tipada, regras e cache no lugar, o módulo está pronto para entrar no cadastro ou na esteira de compras. Se parte do time usa Python, a versão do guia para consultar CNPJ com Python na APIBrasil usa o mesmo endpoint e os mesmos campos.

Para rodar com segurança em escala, o guia de boas práticas de integração em produção cobre tokens, retries e monitoramento de saldo. Crie sua conta em platform.apibrasil.com.br e comece em homologação: o código acima roda sem custo até você decidir ligar a base real.

Loading

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *