# Como consultar a inscrição estadual de milhares de CNPJs em Node.js?

> Script pronto com o SDK oficial: segue o limite do seu plano, para quando acabam as consultas e retoma de onde parou.

URL: https://docs.fiscalapi.com.br/docs/guias/node-lote
Atualizado em 07/11/2026

Use o SDK oficial `fiscalapi-br` e o script desta página: ele consulta um CNPJ por vez no ritmo do seu plano, grava cada resposta na hora e, se parar (queda, saldo, `Ctrl+C`), continua de onde parou.

* **Custo:** 1 consulta por CNPJ, de R$ 0,025 a R$ 0,67 conforme o plano ([tabela](#quanto-custa)).
* **Grátis para testar a API:** 10 consultas por mês de IE em um estado e de CNPJ; a consulta nos 27 estados usada neste guia é paga ([condições](#quanto-custa)).
* **Ritmo:** 5 a 120 CNPJs por minuto, conforme o plano.
* **Comece:** [crie a conta e a chave de API](https://app.fiscalapi.com.br?utm_source=docs\&utm_medium=docs\&utm_campaign=guia-node).

## Antes de começar

* Node.js 18 ou mais novo. O SDK usa o `fetch` nativo e não tem dependências.
* A chave de API na variável de ambiente `FISCALAPI_API_KEY`. Use-a só no servidor, nunca no navegador: ela dá acesso ao seu saldo.
* Um arquivo `cnpjs.txt` com um CNPJ por linha (pontuação é aceita).

```bash
npm install fiscalapi-br
export FISCALAPI_API_KEY="fapi_sua_chave_aqui"
```

Primeira chamada isolada: [Quickstart](/docs/getting-started/quickstart). Métodos do SDK: [fiscalapi-node](https://github.com/fiscalapi-br/fiscalapi-node).

## O script

Salve como `consultar-lote.mjs`:

```javascript
// Consulta a IE de uma lista de CNPJs nos 27 estados, com retomada.
import { appendFileSync, existsSync, readFileSync } from 'node:fs';
import { setTimeout as dormir } from 'node:timers/promises';
import {
  APIConnectionError,
  AuthenticationError,
  BadRequestError,
  FiscalAPI,
  InsufficientBalanceError,
  RateLimitError,
  ServerError,
} from 'fiscalapi-br';

const ENTRADA = 'cnpjs.txt'; // um CNPJ por linha
const SAIDA = 'resultado.jsonl'; // uma linha por CNPJ concluído (é também o checkpoint)
const FALHAS = 'falhas.jsonl'; // 5xx e falhas de rede: entram de novo na próxima rodada

// Milissegundos entre chamadas, pelo limite por minuto do plano.
const PAUSA = { lite: 12000, sem_plano: 12000, starter: 3000, pro: 1000, enterprise: 500 };
const PLANO = 'starter';

const limpar = (valor) => valor.replace(/[^0-9a-z]/gi, '').toUpperCase();

// Dígitos verificadores, também do CNPJ alfanumérico. CNPJ com DV errado não
// volta erro da API: é consultado e cobrado. Melhor barrar antes.
function cnpjValido(cnpj) {
  if (!/^[0-9A-Z]{12}[0-9]{2}$/.test(cnpj) || /^(.)\1+$/.test(cnpj)) return false;
  const v = [...cnpj].map((c) => c.charCodeAt(0) - 48);
  for (const n of [12, 13]) {
    let soma = 0;
    for (let i = 0; i < n; i++) soma += v[i] * (((n - 1 - i) % 8) + 2);
    const resto = soma % 11;
    if (v[n] !== (resto < 2 ? 0 : 11 - resto)) return false;
  }
  return true;
}

function jaConcluidos() {
  if (!existsSync(SAIDA)) return new Set();
  return new Set(
    readFileSync(SAIDA, 'utf8')
      .split('\n')
      .filter(Boolean)
      .map((linha) => JSON.parse(linha).cnpj),
  );
}

function gravar(arquivo, registro) {
  registro.consultado_em = new Date().toISOString();
  appendFileSync(arquivo, JSON.stringify(registro) + '\n');
}

const api = new FiscalAPI(); // lê FISCALAPI_API_KEY; já repete sozinho em 429 e 5xx
const pausa = PAUSA[PLANO];

const feitos = jaConcluidos();
const pendentes = [
  ...new Set(
    readFileSync(ENTRADA, 'utf8')
      .split('\n')
      .map(limpar)
      .filter((cnpj) => cnpj && !feitos.has(cnpj)),
  ),
];
console.log(`${feitos.size} já concluídos, ${pendentes.length} pendentes`);

let ultima = 0;
for (const [i, cnpj] of pendentes.entries()) {
  if (!cnpjValido(cnpj)) {
    gravar(SAIDA, { cnpj, erro: 'CNPJ_INVALIDO' }); // não chama a API
    continue;
  }

  const espera = pausa - (Date.now() - ultima);
  if (espera > 0) await dormir(espera);
  ultima = Date.now();

  let resp;
  try {
    resp = await api.consultarIeTodos({ cnpj });
  } catch (e) {
    if (e instanceof InsufficientBalanceError) {
      // 402: acabaram as consultas do mês e não há saldo de recarga. Repetir não adianta.
      console.error(`Parei no ${cnpj}: ${e.message}. Rode de novo depois de recarregar.`);
      process.exit(2);
    }
    if (e instanceof AuthenticationError) {
      // 403: chave ausente, inválida ou revogada.
      console.error(`Chave recusada: ${e.message}`);
      process.exit(3);
    }
    if (e instanceof RateLimitError) {
      // 429 mesmo depois das tentativas do SDK: espera e deixa para a próxima rodada.
      const segundos = e.retryAfter ?? 60;
      console.warn(`Limite por minuto atingido; esperando ${segundos} s`);
      await dormir(segundos * 1000);
      continue;
    }
    if (e instanceof BadRequestError) {
      // 400: documento ausente ou sem dígitos. Grava como concluído.
      gravar(SAIDA, { cnpj, erro: e.code, mensagem: e.message });
      continue;
    }
    if (e instanceof ServerError || e instanceof APIConnectionError) {
      gravar(FALHAS, { cnpj, erro: e.status ?? null, mensagem: e.message });
      continue;
    }
    throw e; // qualquer outro erro: para tudo
  }

  const inscricoes = resp.results.flatMap((estado) =>
    estado.results.map((ie) => ({
      uf: estado.uf,
      ie: ie.inscricao_estadual,
      situacao: ie.situacao_ie,
    })),
  );
  gravar(SAIDA, { cnpj, complete: resp.complete ?? true, inscricoes });
  if ((i + 1) % 100 === 0) console.log(`${i + 1}/${pendentes.length}`);
}

console.log('Fim. Os que caíram em falhas.jsonl entram de novo se você rodar outra vez.');
```

Rode com `node consultar-lote.mjs`. Ajuste `PLANO` para o seu plano.

## Como ele lida com cada situação

| Situação                           | O que o script faz                                                                                                                                                                                                   |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Limite por minuto                  | Espera `PAUSA[PLANO]` entre uma chamada e a seguinte: 12 s no Lite e sem plano, 3 s no Starter, 1 s no Pro, 0,5 s no Enterprise. O limite é da conta, somando todas as chaves: não rode dois scripts ao mesmo tempo. |
| `429`                              | O SDK tenta de novo até 3 vezes, respeitando o `Retry-After` (até 60 s). Se ainda vier `RateLimitError`, o script espera e deixa o CNPJ para a próxima rodada.                                                       |
| CNPJ com dígito verificador errado | `cnpjValido` barra antes e grava `CNPJ_INVALIDO`. Na consulta nos 27 estados, a API não confere o dígito: CNPJ errado seria cobrado.                                                                                 |
| `402`                              | `InsufficientBalanceError`: acabaram as consultas do mês e não há recarga. O script para na hora. Recarregue ou mude de plano e rode de novo.                                                                        |
| `400`                              | `BadRequestError`: documento ausente ou sem dígitos. Vai para o resultado com o erro e não é repetido.                                                                                                               |
| `5xx` ou rede                      | O SDK tenta de novo em `500`, `502`, `503` e `504`. Se persistir, o CNPJ vai para `falhas.jsonl` e entra na próxima rodada.                                                                                          |
| `403`                              | `AuthenticationError`: chave ausente, inválida ou revogada. O script para.                                                                                                                                           |
| Interrupção                        | `resultado.jsonl` é o checkpoint: na próxima rodada, os CNPJs concluídos são pulados.                                                                                                                                |

`complete: false` quer dizer que algum estado não respondeu, e aí uma lista vazia não prova que não há IE. A consulta foi cobrada (pelo menos um estado respondeu), por isso o script a grava como concluída. Para refazer só esses CNPJs, filtre `"complete":false` no `resultado.jsonl` e rode de novo; cada um é uma nova consulta.

## Quanto tempo leva

O teto é o do plano: 5 CNPJs por minuto no Lite, 20 no Starter, 60 no Pro e 120 no Enterprise. Ou seja, 1.000 CNPJs levam pelo menos 50 minutos no Starter e 17 no Pro. Como o script faz uma chamada de cada vez, o tempo de resposta também pesa e, no Pro e no Enterprise, o ritmo costuma ficar abaixo do teto. Para chegar perto, rode algumas chamadas em paralelo (por exemplo, com `Promise.all` em grupos pequenos) sem passar do limite.

Sem programar, o painel aceita planilhas de até 20.000 linhas: veja [consulta de IE em massa](https://fiscalapi.com.br/consulta-ie-em-massa/).

## Variações

* **Só um estado**: `api.consultarIe({ uf: 'MT', cnpj })` devolve as inscrições direto em `resp.results`.
* **Produtor rural por CPF**: `api.consultarIeTodos({ cpf: '12345678909' })`.
* **Dados da Receita Federal**: `api.consultarCnpj({ cnpj })` devolve `resp.result.razao_social`, CNAE, QSA e endereço. É mais uma consulta por CNPJ.

## Quanto custa

|                                 | Lite        | Starter | Pro      | Enterprise |
| ------------------------------- | ----------- | ------- | -------- | ---------- |
| Consulta de IE (1 UF ou 27 UFs) | R$ 0,67     | R$ 0,10 | R$ 0,065 | R$ 0,025   |
| Recarga, por consulta           | sem recarga | R$ 0,12 | R$ 0,07  | R$ 0,02    |
| Consultas por minuto            | 5           | 20      | 60       | 120        |

Cada CNPJ é uma consulta. No Enterprise, 10.000 CNPJs cabem na franquia de R$ 250 do mês. Resposta com erro HTTP (400, 402, 429, 500) não é cobrada. Veja [Planos e limites](/docs/referencia/plans-and-limits).

**Grátis para testar:** conta que nunca assinou plano, sem recarga e com e-mail confirmado, tem 10 consultas por mês de IE em um estado e de CNPJ, também pela API. Teste com `api.consultarIe({ uf: 'MT', cnpj })`: a consulta nos 27 estados usa plano ou recarga. [Criar conta](https://app.fiscalapi.com.br?utm_source=docs\&utm_medium=docs\&utm_campaign=guia-node).

## Perguntas frequentes

### Existe SDK da FiscalAPI para Node.js?

Sim, o pacote oficial `fiscalapi-br` no npm (`npm install fiscalapi-br`). Tem tipos TypeScript, funciona com ESM e CommonJS, usa o `fetch` nativo do Node 18+ e tenta de novo sozinho em 429 e em falhas temporárias.

### Como consultar inscrição estadual de vários CNPJs em Node.js?

Chame `api.consultarIeTodos({ cnpj })` para cada CNPJ, com uma pausa entre as chamadas conforme o plano, e grave cada resposta assim que ela chega. O script desta página faz isso e retoma de onde parou.

### Como tratar o erro 429 da FiscalAPI em JavaScript?

Esperar os segundos do header `Retry-After` e tentar de novo. O SDK já faz isso até 3 vezes; depois, lança `RateLimitError` com `retryAfter`.

### O que significa o erro 402?

Que acabaram as consultas do mês (as grátis ou a franquia do plano) e não há recarga. O SDK lança `InsufficientBalanceError`. Repetir não resolve: recarregue ou mude de plano.

### Posso chamar a FiscalAPI direto do navegador?

Não deve. A chave de API daria acesso ao seu saldo a qualquer visitante. Chame a API do seu servidor.
