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
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
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
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"
{"valid":true,"type":"AR_CUIT","country":"AR","input":"20-12345678-6","normalized":"20123456786","formatted":"20-12345678-6","error":null,"details":{"entity":"person","dni":"12345678"}}
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"}'
{"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}}
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}
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"])
CL_RUT True 11.111.111-1
MX_CLABE True 002 010 07777777777 1
BR_CPF False repeated digits are invalid
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}`);
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",...}, ...]}
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"] }
}
}
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)