DEV Community

Matias Pengue
Matias Pengue

Posted on Edited on

Como validar CPF e CNPJ (inclusive o CNPJ alfanumérico) com código — e uma API para 19 documentos da América Latina

Se você já construiu um cadastro, checkout ou fluxo de faturamento para o Brasil (ou para a América Latina), já esbarrou nisso: CPF e CNPJ carregam dígitos verificadores que pegam erros de digitação antes de chegarem ao seu banco de dados — e cada país vizinho tem seus próprios formatos.

Os algoritmos são públicos, mas os snippets que circulam por aí costumam errar nos casos de borda — e, com o CNPJ alfanumérico de 2026, muita validação antiga vai quebrar. Aqui está como CPF e CNPJ funcionam de verdade, mais alguns documentos de outros países, e no final uma API gratuita que criei para validar 19 tipos.

🇬🇧 English version: https://dev.to/ialatam/how-check-digits-work-for-latin-american-ids-cuit-cpf-rfc-and-an-api-that-validates-19-of-them-4cj4

👉 Exemplos prontos para copiar (curl, Python, JS): validalatam.ia-latam.app/pt

CPF: dois dígitos verificadores

O CPF tem 9 dígitos base + 2 dígitos verificadores. O primeiro usa pesos 10..2; o segundo usa 11..2 sobre os 10 primeiros dígitos. Nos dois casos o dígito é (soma * 10) % 11, e 10 vira 0. Sequências de um único dígito repetido, como 111.111.111-11, passam na conta, mas são explicitamente inválidas.

def cpf_ok(c: str) -> bool:
    if len(set(c)) == 1:
        return False
    for n in (9, 10):
        s = sum(int(c[i]) * (n + 1 - i) for i in range(n))
        if (s * 10) % 11 % 10 != int(c[n]):
            return False
    return True

cpf_ok("11144477735")  # True
Enter fullscreen mode Exit fullscreen mode

CNPJ (inclusive o alfanumérico)

O CNPJ segue a mesma ideia de dois dígitos verificadores, com pesos diferentes: 12 posições base (8 da raiz + 4 da filial, como 0001) e 2 dígitos verificadores, calculados com módulo 11.

A partir de 2026, a Receita Federal passa a emitir CNPJ alfanumérico: as 12 primeiras posições podem conter letras, e os dois verificadores continuam numéricos. O cálculo é o mesmo, mas o valor de cada caractere passa a ser ord(ch) - 48, ou seja, 0..9 continuam valendo 0..9 e A = 17, B = 18, etc. Se o seu validador faz int(ch), ele vai quebrar.

Outros países

  • CUIT / CUIL (Argentina): módulo 11 com pesos 5,4,3,2,7,6,5,4,3,2; resultado 11 vira 0 e 10 nunca é válido.
def cuit_ok(c: str) -> bool:
    w = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2]
    dv = 11 - sum(int(a) * b for a, b in zip(c[:10], w)) % 11
    return dv != 10 and (0 if dv == 11 else dv) == int(c[10])

cuit_ok("20123456786")  # True
cuit_ok("20123456780")  # False
Enter fullscreen mode Exit fullscreen mode

O RUC do Peru usa os mesmos pesos, mas mapeia 10 → 0 e 11 → 1, e o RUT do Chile percorre da direita para a esquerda com pesos cíclicos 2..7, onde 10 vira K.

  • RFC (México): 4 letras + data de nascimento (AAMMDD) + homoclave de 3 caracteres, mapeados por um dicionário de 39 símbolos — o & e o Ñ são onde validadores caseiros costumam falhar:
D = "0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ Ñ"

def rfc_ok(r: str) -> bool:
    r = r.upper().rjust(13)          # company RFCs (12 chars) are left-padded
    s = sum(D.index(ch) * (13 - i) for i, ch in enumerate(r[:12]))
    dv = 11 - s % 11
    expected = "0" if dv == 11 else "A" if dv == 10 else str(dv)
    return r[12] == expected

rfc_ok("GODE561231GR8")  # True
Enter fullscreen mode Exit fullscreen mode
  • CLABE (MX) e CBU/CVU (AR) usam módulo 10 ponderado (3,7,1 na CLABE; dois blocos no CBU)
  • CURP (MX) mapeia A=10 … Ñ=24 … Z=36, pesos 18..2, módulo 10
  • IBAN: move os 4 primeiros caracteres para o final, converte letras em números, mod 97 == 1

Um dígito verificador válido significa que o número está bem formado, não que ele existe. Só a Receita (ou o órgão fiscal de cada país) pode confirmar isso. Mesmo assim, ele pega a maioria dos erros de digitação na hora do preenchimento, sem nenhuma chamada a serviço do governo.

Usando a API

Empacotei tudo isso na ValidaLatam, uma API stateless que valida e formata 19 tipos: AR_CUIT, AR_CUIL, AR_DNI, AR_CBU, AR_CVU, BR_CPF, BR_CNPJ, MX_RFC, MX_CURP, MX_CLABE, CL_RUT, CO_NIT, PE_RUC, UY_RUT, UY_CI, EC_CEDULA, EC_RUC, PY_RUC e IBAN. É determinística, não armazena nada e tem CORS aberto. Documentação: https://validalatam.ia-latam.app

A API direta é gratuita para testar (com limite diário).

curl

curl "https://validalatam.ia-latam.app/validate/AR_CUIT?value=20-12345678-6"
Enter fullscreen mode Exit fullscreen mode
{"valid":true,"type":"AR_CUIT","country":"AR","input":"20-12345678-6","normalized":"20123456786","formatted":"20-12345678-6","error":null,"details":{"entity":"person","dni":"12345678"}}
Enter fullscreen mode Exit fullscreen mode

Se você não sabe qual é o tipo, use AUTO — por exemplo, com um CNPJ:

curl -X POST https://validalatam.ia-latam.app/validate \
  -H 'content-type: application/json' \
  -d '{"type":"AUTO","value":"11.222.333/0001-81"}'
Enter fullscreen mode Exit fullscreen mode
{"valid":true,"type":"BR_CNPJ","country":"BR","input":"11.222.333/0001-81","normalized":"11222333000181","formatted":"11.222.333/0001-81","error":null,"details":{"branch":"0001","alphanumeric":false}}
Enter fullscreen mode Exit fullscreen mode

Valores inválidos retornam valid: false com o motivo:

{"valid":false,"type":"AR_CUIT","country":"AR","input":"20123456780","normalized":"20123456780","formatted":null,"error":"check digit mismatch","details":null}
Enter fullscreen mode Exit fullscreen mode

Python (batch)

import requests

r = requests.post("https://validalatam.ia-latam.app/batch", json={"items": [
    {"type": "CL_RUT", "value": "11.111.111-1"},
    {"value": "002010077777777771", "country": "MX"},   # AUTO + country hint
    {"type": "BR_CPF", "value": "111.111.111-11"},
]})
for res in r.json()["results"]:
    print(res["type"], res["valid"], res["formatted"] or res["error"])
Enter fullscreen mode Exit fullscreen mode
CL_RUT True 11.111.111-1
MX_CLABE True 002 010 07777777777 1
BR_CPF False repeated digits are invalid
Enter fullscreen mode Exit fullscreen mode

O endpoint batch aceita até 1000 itens.

JavaScript (navegador ou Node)

Como o CORS é aberto, dá para chamar direto de um formulário:

const res = await fetch(
  "https://validalatam.ia-latam.app/validate/MX_RFC?value=GODE561231GR8"
);
const { valid, formatted, error } = await res.json();
console.log(valid ? `OK: ${formatted}` : `Invalid: ${error}`);
Enter fullscreen mode Exit fullscreen mode

Dados de teste

GET /generate/{type}?count=N&seed=S retorna valores sintéticos com formato válido para fixtures. Cada um vem marcado com "fake": true.

curl "https://validalatam.ia-latam.app/generate/BR_CPF?count=2&seed=42"
# {"fake":true,"items":[{"value":"15860461607","formatted":"158.604.616-07",...}, ...]}
Enter fullscreen mode Exit fullscreen mode

Para produção, com chave e rate limits, há uma listagem na RapidAPI com plano gratuito (500 req/mês): https://rapidapi.com/MatiasPengue/api/validalatam

Para agentes de IA: servidor MCP

LLMs são péssimos com dígitos verificadores: eles vão afirmar com toda a confiança que um CPF é válido. Se você usa Claude Desktop, Cursor ou outro cliente MCP, pode dar ao agente uma ferramenta determinística:

{
  "mcpServers": {
    "validalatam": { "command": "uvx", "args": ["validalatam-mcp"] }
  }
}
Enter fullscreen mode Exit fullscreen mode

Código-fonte: https://github.com/elmatis172/validalatam-mcp (também está no registro oficial de MCP). Aí você pode pedir coisas como "valide estes 50 CNPJs da planilha e formate os válidos", e o agente chama a ferramenta em vez de chutar.

Também há um actor no Apify para processamento em lote: https://apify.com/ialatam/latam-document-validator

Concluindo

Se você só precisa de CPF e CNPJ, os snippets acima bastam. Copie e adicione testes com valores conhecidos. Se você lida com vários países, ou quer formatação e detecção automática, a API pode economizar seu tempo.

👉 Exemplos prontos para copiar (curl, Python, JS): validalatam.ia-latam.app/pt

Sou o Matias, desenvolvedor da Argentina. Qual documento devo adicionar a seguir? NIT da Bolívia, RIF da Venezuela e cédula da Costa Rica estão na lista. Relatos de bugs em casos de borda são muito bem-vindos.

Top comments (0)