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.

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.
| Campo | Valores | Regra típica |
|---|---|---|
| situacao_cadastral | 01 nula, 02 ativa, 03 suspensa, 04 inapta, 08 baixada | Só aceitar 02 |
| cnae_fiscal e cnae_fiscal_secundaria | Códigos de 7 dígitos | Exigir ao menos um CNAE compatível com o serviço contratado |
| empresa.porte_empresa | 01 ME, 03 EPP, 05 demais | Alçada de aprovação por porte |
| empresa.capital_social | Valor em reais | Mínimo por categoria de contrato |
| simples.opcao_simples / opcao_mei | S ou N | Define retenção de impostos e tipo de nota fiscal |
| data_inicio_atividades | AAAAMMDD | Tempo 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.
![]()









