DEV Community

Matias Pengue
Matias Pengue

Posted on

Cómo funciona el dígito verificador de CUIT, RFC, CURP y CLABE (con código) — y una API que valida 19 documentos de Latinoamérica

Si alguna vez armaste un registro, un checkout o un flujo de facturación para Latinoamérica, ya te cruzaste con este problema: cada país tiene su propio formato de identificación fiscal, documento de identidad y cuenta bancaria, y casi todos llevan un dígito verificador que detecta errores de tipeo antes de que lleguen a tu base de datos.

Los algoritmos son públicos, pero están desparramados en PDFs de AFIP/ARCA, el SAT y el BCRA, y los snippets que circulan en gists suelen fallar en los casos borde. Acá te muestro cómo funcionan los más comunes (CUIT/CUIL, CBU/CVU, RFC, CURP y CLABE) y al final, una API gratuita que armé y que valida 19 tipos de documentos.

🇬🇧 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

CUIT / CUIL (Argentina): módulo 11 con pesos fijos

Un CUIT tiene 11 dígitos: un prefijo de 2 dígitos (20/23/24/27 para personas, 30/33/34 para empresas), un cuerpo de 8 dígitos (el DNI, en el caso de personas) y 1 dígito verificador.

Multiplicás los primeros 10 dígitos por 5,4,3,2,7,6,5,4,3,2, sumás y calculás 11 − (suma % 11). Si el resultado es 11, el dígito es 0. Si da 10, nunca es válido: para evitarlo, AFIP asigna el prefijo 23/33.

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

El RUC de Perú usa los mismos pesos pero mapea 10 → 0 y 11 → 1, y el RUT de Chile recorre de derecha a izquierda con pesos cíclicos 2..7, donde el 10 se convierte en K. Son de la misma familia, pero no podés copiar uno en otro.

CBU / CVU (Argentina): módulo 10 en dos bloques

El CBU (y el CVU de las billeteras virtuales) tiene 22 dígitos divididos en dos bloques, cada uno con su propio verificador: el primer bloque (8 dígitos: banco + sucursal) y el segundo (14 dígitos: la cuenta). Cada bloque usa un módulo 10 ponderado.

RFC (México): letras, & y Ñ

Un RFC de persona física tiene 4 letras + fecha de nacimiento (AAMMDD) + homoclave de 3 caracteres, donde el último es el dígito verificador. Cada carácter se mapea con un diccionario de 39 símbolos:

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

La & y la Ñ del diccionario son justamente donde fallan los validadores hechos a mano.

CURP y CLABE (México)

  • CURP: mapea A=10 … Ñ=24 … Z=36, pesos 18..2, módulo 10.
  • CLABE: 18 dígitos con módulo 10 ponderado usando los pesos 3,7,1.

CPF (Brasil): dos dígitos verificadores

El CPF tiene 9 dígitos base + 2 verificadores. El primero usa pesos 10..2 y el segundo 11..2 sobre los primeros 10 dígitos. En ambos casos el dígito es (suma * 10) % 11, y 10 pasa a ser 0. Las secuencias de un mismo dígito repetido como 111.111.111-11 pasan la cuenta pero son explícitamente 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

El CNPJ sigue la misma idea de dos dígitos con otros pesos. Desde 2026 también puede ser alfanumérico: el valor de cada carácter es ord(ch) - 48, o sea A = 17.

Los demás

  • IBAN: movés los primeros 4 caracteres al final, convertís las letras en números y verificás mod 97 == 1.

Ojo: que el dígito verificador sea válido significa que el número está bien formado, no que exista. Eso solo te lo puede confirmar el organismo fiscal. Igual, detecta la mayoría de los errores de tipeo al momento de la carga, sin ir a consultar a un servicio del gobierno.

Usando la API

Empaqueté todo esto en ValidaLatam, una API stateless que valida y formatea 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. Es determinística, no guarda nada y tiene CORS abierto. Documentación: https://validalatam.ia-latam.app

La API directa es gratis para probar (con un límite diario).

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

Si no sabés qué tipo de documento tenés, usá AUTO:

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

Los valores inválidos devuelven valid: false con el 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

El endpoint batch acepta hasta 1000 ítems.

JavaScript (navegador o Node)

Como el CORS está abierto, podés llamarla directamente desde un formulario:

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

Datos de prueba

GET /generate/{type}?count=N&seed=S devuelve valores sintéticos con formato válido para tus fixtures. Cada uno viene marcado con "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 producción, con API key y rate limits, está el listado en RapidAPI con un plan gratuito (500 req/mes): https://rapidapi.com/MatiasPengue/api/validalatam

Para agentes de IA: servidor MCP

Los LLMs son malísimos con los dígitos verificadores: te van a decir muy seguros que un CUIT es válido. Si usás Claude Desktop, Cursor u otro cliente MCP, podés darle al agente una herramienta determinística:

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

Código fuente: https://github.com/elmatis172/validalatam-mcp (también está en el registro oficial de MCP). Después podés pedirle cosas como "validá estos 50 CUITs de la planilla y formateá los válidos", y el agente usa la herramienta en lugar de adivinar.

También hay un actor de Apify para procesos masivos: https://apify.com/ialatam/latam-document-validator

Para cerrar

Si solo necesitás un documento, con los snippets de arriba alcanza. Copialos y agregales tests con valores conocidos. Si trabajás con varios países, o querés formateo y detección automática, la API te puede ahorrar tiempo.

Soy Matias, desarrollador de Argentina. ¿Qué documento debería agregar ahora? El NIT de Bolivia, el RIF de Venezuela y la cédula de Costa Rica están en mi lista. Los reportes de bugs en casos borde son más que bienvenidos.

Top comments (0)