DEV Community

Cover image for 5 pegadinhas que aprendi automatizando CND Federal, CNDT e FGTS em Python
Luís Eduardo Souza Barros
Luís Eduardo Souza Barros

Posted on

5 pegadinhas que aprendi automatizando CND Federal, CNDT e FGTS em Python

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"
Enter fullscreen mode Exit fullscreen mode

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..."
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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"}
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)])
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)