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_statusdiz se o órgão respondeu. - O
protocoloé o número para conferir a autenticidade. - O
pdf_base64traz 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
resultvemnulle osource_statustrazstatus: "error"com umerror_code; - ou o
result.statusvem"erro", com o motivo nostatus_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.
