Cadastro de fornecedor tem uma rotina que ninguém gosta: abrir o site da Receita, o do TST e o da Caixa, digitar o CNPJ, baixar o PDF e repetir. Com cinco fornecedores, dá. Com cinquenta, todo mês, vira o dia inteiro de alguém.

Eu trabalho na FiscalAPI, e a gente transformou essas três consultas em endpoints. No caminho apareceram cinco pegadinhas. Três delas valem para quem automatiza isso de qualquer jeito. As outras duas são sobre como tratar erro e demora, e nelas mostro como a nossa API responde. No fim tem um script em Python que lê uma lista de CNPJs e gera uma planilha.

TL;DR

  • "Positiva" nem sempre é irregular: a positiva com efeitos de negativa vale como negativa.
  • Cada certidão vence num prazo diferente. O CRF do FGTS dura 30 dias e só renova nos 5 dias finais.
  • Erro ao consultar não é "irregular" nem "regular": é "pendente".
  • A CND Federal é da matriz.
  • A CND Federal pode levar perto de um minuto para ser emitida.

As três certidões

  • CND Federal: débitos de tributos federais e da Dívida Ativa da União (Receita + PGFN).
  • CNDT: débitos trabalhistas (TST).
  • CRF: o Certificado de Regularidade do FGTS (Caixa).

A Lei 14.133/2021, no art. 68, pede regularidade com a Fazenda federal, com a Seguridade Social e o FGTS, e com a Justiça do Trabalho. Os três órgãos emitem as certidões de graça nos próprios sites (Receita/PGFN, TST, Caixa).

A chamada e a resposta

Cada certidão tem o próprio endpoint, e todos recebem o documento por query string:

curl "https://api.fiscalapi.com.br/api/consultar-cndt?cnpj=12345678000195" \ -H "X-API-Key: fapi_sua_chave_aqui" 

A resposta tem o mesmo formato nas três. Segue um trecho:

{ "document": "12345678000195", "document_type": "cnpj", "source_status": { "status": "success", "error_code": "", "error_message": "" }, "result": { "tipo": "cndt", "status": "negativa", "status_raw": "CERTIDAO NEGATIVA DE DEBITOS TRABALHISTAS", "validade_iso": "2027-05-02", "emissao_iso": "2026-11-03", "protocolo": "5945624/2026", "orgao": "TST", "pdf_base64": "JVBERi0xLjQK..." } } 
  • O source_status diz se o órgão respondeu.
  • O protocolo é o número para conferir a autenticidade.
  • O pdf_base64 traz o PDF quando o órgão emite a certidão. Na CND Federal e no CRF, ele vem vazio se houver pendência. A CNDT positiva vem com PDF.

Os outros dois endpoints são /api/consultar-cnd-federal e /api/consultar-crf-fgts.

As 5 pegadinhas

1. "Positiva" nem sempre quer dizer irregular

Existe a positiva com efeitos de negativa: há débito, mas ele está suspenso ou garantido (parcelamento, decisão judicial, penhora). Para a lei, ela vale como negativa, pelo art. 206 do CTN e, na CNDT, pelo art. 642-A, § 2º, da CLT. Se o seu código só aceitar negativa, você vai barrar fornecedor regular.

REGULAR = {"negativa", "positiva_com_efeitos_de_negativa"} 

2. Cada certidão vence num prazo diferente

A CND Federal e a CNDT valem 180 dias. O CRF do FGTS vale 30 dias e, pelo manual de regularidade da Caixa, só pode ser renovado a partir do quinto dia antes de vencer. Um cron fixo do tipo "consulta tudo todo dia 1º" ou gasta consulta à toa ou deixa o CRF vencer. O mais simples é marcar, para cada certidão, a data de olhar de novo:

from datetime import date, timedelta # CND Federal: a API só emite outra quando faltam 3 dias ou menos; antes disso devolve a mesma. # CNDT: não há regra de renovação; 10 dias é só margem (escolha nossa). # CRF: a Caixa só renova a partir do 5º dia antes do vencimento. FOLGA = {"cnd_federal": 3, "cndt": 10, "crf_fgts": 5} def reconsultar_em(tipo: str, validade_iso: str) -> date: return date.fromisoformat(validade_iso) - timedelta(days=FOLGA[tipo]) 

3. Erro não é "irregular", e também não é "regular"

Às vezes o órgão não responde, ou a certidão não pode ser confirmada. Isso não quer dizer que o fornecedor está irregular, nem que está regular: quer dizer que você ainda não sabe.

Na nossa API, isso aparece de dois jeitos:

  • o result vem null e o source_status traz status: "error" com um error_code;
  • ou o result.status vem "erro", com o motivo no status_raw.

No seu fluxo, os dois viram "pendente":

def situacao(data: dict) -> str: status = (data.get("result") or {}).get("status") if not status or status == "erro": return "pendente" # o órgão não confirmou: você ainda não sabe  return "regular" if status in REGULAR else "irregular" 

4. A CND Federal é da matriz

A Receita emite a certidão para o CNPJ da matriz, e ela vale para as filiais. Se você mandar o CNPJ de uma filial, a resposta vem com status: "erro" e o motivo. Para pessoa física, a Receita pede CPF e data de nascimento. Por isso, na API, a CND Federal é só por CNPJ. CNDT e CRF aceitam CPF.

As três esperam o CNPJ completo, com 14 dígitos. Se a sua lista vem de planilha, com máscara ou sem o zero da frente (o Excel come), normalize antes:

import re def normalizar(cnpj: str) -> str: cnpj = re.sub(r"[./\s-]", "", cnpj) # tira só a máscara  return cnpj.zfill(14) if cnpj.isdigit() else cnpj normalizar("01.234.567/0001-95") # "01234567000195" normalizar("1234567000195") # "01234567000195" 

5. A CND Federal pode demorar

A emissão na Receita pode levar perto de um minuto. Se o seu cliente HTTP tem timeout de 30 s, ele desiste antes.

Na API, a certidão é emitida na hora em dois casos: quando não existe certidão válida ou quando a atual vence em até 3 dias. Se ela não ficar pronta a tempo, a resposta chega assim, com o header Retry-After:

{ "document": "12345678000195", "source_status": { "status": "error", "error_code": "EM_PROCESSAMENTO", "error_message": "A emissao desta certidao esta em andamento. Consulte novamente em cerca de 45 s." }, "result": null } 

Consultar de novo não dispara outra emissão. Basta esperar o tempo do Retry-After e perguntar de novo.

O script completo

O script lê um CNPJ por linha de fornecedores.txt e gera certidoes.csv. Para cada certidão, a planilha traz:

  • a situação;
  • a validade;
  • a data para olhar de novo;
  • o número;
  • o detalhe (o texto do órgão ou o código do erro).
import csv import re import time from datetime import date, timedelta import requests API = "https://api.fiscalapi.com.br/api" HEADERS = {"X-API-Key": "fapi_sua_chave_aqui"} ENDPOINTS = { "cnd_federal": "consultar-cnd-federal", "cndt": "consultar-cndt", "crf_fgts": "consultar-crf-fgts", } REGULAR = {"negativa", "positiva_com_efeitos_de_negativa"} FOLGA = {"cnd_federal": 3, "cndt": 10, "crf_fgts": 5} # ver pegadinha 2  def consultar(endpoint: str, cnpj: str, tentativas: int = 5) -> dict: data: dict = {"result": None, "source_status": {"error_code": "SEM_RESPOSTA"}} for tentativa in range(tentativas): r = requests.get(f"{API}/{endpoint}", headers=HEADERS, params={"cnpj": cnpj}, timeout=120) if r.status_code in (401, 402, 403): # chave errada ou sem saldo: não adianta seguir  raise SystemExit(f"HTTP {r.status_code}: {r.text[:200]}") if r.status_code == 429: # passou do limite por minuto do plano  espera = int(r.headers.get("Retry-After", 60)) elif r.status_code >= 400: return {"result": None, "source_status": {"error_code": f"HTTP_{r.status_code}"}} else: data = r.json() if (data.get("source_status") or {}).get("error_code") != "EM_PROCESSAMENTO": return data espera = int(r.headers.get("Retry-After", 30)) # pegadinha 5  if tentativa < tentativas - 1: time.sleep(espera) return data def situacao(data: dict) -> str: status = (data.get("result") or {}).get("status") if not status or status == "erro": return "pendente" return "regular" if status in REGULAR else "irregular" def detalhe(data: dict) -> str: res = data.get("result") or {} return res.get("status_raw") or (data.get("source_status") or {}).get("error_code", "") def normalizar(cnpj: str) -> str: cnpj = re.sub(r"[./\s-]", "", cnpj) return cnpj.zfill(14) if cnpj.isdigit() else cnpj with open("fornecedores.txt", encoding="utf-8-sig") as f: cnpjs = [normalizar(linha) for linha in f if linha.strip()] with open("certidoes.csv", "w", newline="", encoding="utf-8-sig") as saida: csv_out = csv.writer(saida, delimiter=";") # ";" abre direto no Excel em português  csv_out.writerow(["cnpj", "certidao", "situacao", "validade", "reconsultar_em", "protocolo", "detalhe"]) for cnpj in cnpjs: for tipo, endpoint in ENDPOINTS.items(): data = consultar(endpoint, cnpj) res = data.get("result") or {} validade = res.get("validade_iso", "") quando = (date.fromisoformat(validade) - timedelta(days=FOLGA[tipo]) if validade else date.today()) csv_out.writerow([cnpj, tipo, situacao(data), validade, quando.isoformat(), res.get("protocolo", ""), detalhe(data)]) 

A saída fica assim (trecho):

cnpj;certidao;situacao;validade;reconsultar_em;protocolo;detalhe 12345678000195;cnd_federal;pendente;;2026-11-03;;EM_PROCESSAMENTO 12345678000195;cndt;regular;2027-05-02;2027-04-22;5945624/2026;CERTIDAO NEGATIVA DE DEBITOS TRABALHISTAS 12345678000100;cnd_federal;pendente;;2026-11-03;;CNPJ_INVALIDO 

A coluna detalhe separa o que é para tentar de novo (EM_PROCESSAMENTO) do que é para corrigir na lista (CNPJ_INVALIDO, filial). Para economizar, guarde o CSV e, na próxima rodada, consulte só as linhas cuja reconsultar_em já chegou. Ao abrir no Excel, importe a coluna cnpj como texto para não perder o zero.

O script está também no GitHub: github.com/fiscalapi-br/certidoes-fornecedores.

E a CND estadual?

Licitação costuma pedir também a regularidade com a Fazenda estadual (o ICMS). Ela tem endpoint próprio, /api/consultar-cnd, que recebe uf e cnpj.

A API é paga, com os planos em fiscalapi.com.br/precos. Erro e EM_PROCESSAMENTO não são cobrados. A documentação, com todos os campos e códigos de erro, está em docs.fiscalapi.com.br.

Se você já faz isso: quais outras certidões entram no seu cadastro de fornecedor? E vocês reconsultam pela validade ou num calendário fixo? Feedback no código também é bem-vindo.