# Como consultar a inscrição estadual de milhares de CNPJs em Python?

> 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/python-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-python).

## Antes de começar

* Python 3.9 ou mais novo. O SDK não tem dependências.
* A chave de API na variável de ambiente `FISCALAPI_API_KEY`.
* Um arquivo `cnpjs.csv` com a coluna `cnpj` (pontuação é aceita).

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

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

## O script

Salve como `consultar_lote.py`:

```python
"""Consulta a IE de uma lista de CNPJs nos 27 estados, com retomada."""
import csv
import json
import re
import sys
import time
from datetime import datetime, timezone

from fiscalapi_br import (
    APIConnectionError,
    AuthenticationError,
    BadRequestError,
    FiscalAPI,
    InsufficientBalanceError,
    RateLimitError,
    ServerError,
)

ENTRADA = "cnpjs.csv"            # coluna "cnpj"
SAIDA = "resultado.jsonl"        # uma linha por CNPJ concluído (é também o checkpoint)
FALHAS = "falhas.jsonl"          # 5xx e falhas de rede: entram de novo na próxima rodada

# Segundos entre chamadas, pelo limite por minuto do plano.
PAUSA = {"lite": 12.0, "sem_plano": 12.0, "starter": 3.0, "pro": 1.0, "enterprise": 0.5}
PLANO = "starter"


def limpar(valor: str) -> str:
    return "".join(c for c in valor if c.isalnum()).upper()


def cnpj_valido(cnpj: str) -> bool:
    """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."""
    if not re.fullmatch(r"[0-9A-Z]{12}[0-9]{2}", cnpj) or len(set(cnpj)) == 1:
        return False
    v = [ord(c) - 48 for c in cnpj]
    for n in (12, 13):
        pesos = list(range(n - 7, 1, -1)) + list(range(9, 1, -1))
        resto = sum(a * b for a, b in zip(v, pesos)) % 11
        if v[n] != (0 if resto < 2 else 11 - resto):
            return False
    return True


def ja_concluidos() -> set[str]:
    try:
        with open(SAIDA, encoding="utf-8") as f:
            return {json.loads(linha)["cnpj"] for linha in f if linha.strip()}
    except FileNotFoundError:
        return set()


def gravar(arquivo: str, registro: dict) -> None:
    registro["consultado_em"] = datetime.now(timezone.utc).isoformat()
    with open(arquivo, "a", encoding="utf-8") as f:
        f.write(json.dumps(registro, ensure_ascii=False) + "\n")


def main() -> None:
    api = FiscalAPI()  # lê FISCALAPI_API_KEY; já repete sozinho em 429 e 5xx
    pausa = PAUSA[PLANO]

    with open(ENTRADA, encoding="utf-8", newline="") as f:
        cnpjs = [limpar(linha["cnpj"]) for linha in csv.DictReader(f)]
    feitos = ja_concluidos()
    pendentes = list(dict.fromkeys(c for c in cnpjs if c and c not in feitos))
    print(f"{len(feitos)} já concluídos, {len(pendentes)} pendentes")

    ultima = 0.0
    for i, cnpj in enumerate(pendentes, 1):
        if not cnpj_valido(cnpj):
            gravar(SAIDA, {"cnpj": cnpj, "erro": "CNPJ_INVALIDO"})  # não chama a API
            continue

        espera = pausa - (time.monotonic() - ultima)
        if espera > 0:
            time.sleep(espera)
        ultima = time.monotonic()

        try:
            resp = api.consultar_ie_todos(cnpj=cnpj)
        except InsufficientBalanceError as e:
            # 402: acabaram as consultas do mês e não há saldo de recarga. Repetir não adianta.
            print(f"Parei no {cnpj}: {e.message}. Rode de novo depois de recarregar.")
            sys.exit(2)
        except AuthenticationError as e:
            # 403: chave ausente, inválida ou revogada.
            print(f"Chave recusada: {e.message}")
            sys.exit(3)
        except RateLimitError as e:
            # 429 mesmo depois das tentativas do SDK: espera e deixa para a próxima rodada.
            print(f"Limite por minuto atingido; esperando {e.retry_after or 60} s")
            time.sleep(e.retry_after or 60)
            continue
        except BadRequestError as e:
            # 400: documento ausente ou sem dígitos. Grava como concluído.
            gravar(SAIDA, {"cnpj": cnpj, "erro": e.code, "mensagem": e.message})
            continue
        except (ServerError, APIConnectionError) as e:
            gravar(FALHAS, {"cnpj": cnpj, "erro": e.status_code, "mensagem": e.message})
            continue

        inscricoes = [
            {"uf": estado["uf"], "ie": ie["inscricao_estadual"], "situacao": ie["situacao_ie"]}
            for estado in resp["results"]
            for ie in estado["results"]
        ]
        gravar(SAIDA, {"cnpj": cnpj, "complete": resp.get("complete", True), "inscricoes": inscricoes})
        if i % 100 == 0:
            print(f"{i}/{len(pendentes)}")

    print("Fim. Os que caíram em falhas.jsonl entram de novo se você rodar outra vez.")


if __name__ == "__main__":
    main()
```

Rode com `python consultar_lote.py`. 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]` segundos entre uma chamada e a seguinte: 12 no Lite e sem plano, 3 no Starter, 1 no Pro, 0,5 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 | `cnpj_valido` 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.                                                                                                                                 |
| CNPJ repetido na lista             | Consultado uma vez só.                                                                                                                                                                                                |

`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 `concurrent.futures.ThreadPoolExecutor`) 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.consultar_ie(uf="MT", cnpj=cnpj)` devolve as inscrições direto em `resp["results"]`.
* **Produtor rural por CPF**: `api.consultar_ie_todos(cpf="12345678909")`.
* **Dados da Receita Federal**: `api.consultar_cnpj(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.consultar_ie(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-python).

## Perguntas frequentes

### Existe biblioteca Python para a FiscalAPI?

Sim, o SDK oficial `fiscalapi-br` no PyPI (`pip install fiscalapi-br`, importado como `fiscalapi_br`). Ele não tem dependências, roda em Python 3.9 ou mais novo e tenta de novo sozinho em 429 e em falhas temporárias.

### Como consultar inscrição estadual de vários CNPJs em Python?

Chame `FiscalAPI().consultar_ie_todos(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.

### O que fazer quando a API devolve 429?

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

### 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 levanta `InsufficientBalanceError`. Repetir não resolve: recarregue ou mude de plano.

### Quantos CNPJs por minuto dá para consultar?

5 no Lite e na conta sem plano, 20 no Starter, 60 no Pro e 120 no Enterprise, somando todas as chaves da conta.
