Publicado em 16 de maio de 2026·10 min de leitura

O custo oculto dos erros de mod-11 no seu onboarding LATAM

Módulo 11 é o algoritmo de dígito verificador mais usado em identificadores tributários da LATAM. É usado por CUIT/CUIL (Argentina), CPF e CNPJ (Brasil), RUT (Chile), NIT (Colômbia), RUC (Peru) e RIF (Venezuela). Mas essa frase é enganadora, porque "mod-11" não descreve um algoritmo único — descreve uma família de algoritmos. Cada país tem sua própria variante com sua própria matriz de pesos, sua própria regra para o caso resto=10, sua própria regra para o caso de padrões uniformes, e sua própria convenção de capitalização. As implementações caseiras que tratam "mod-11" como um único cálculo e só mudam o comprimento são as que geram onboardings quebrados, falsos negativos em fluxos B2B e batch jobs que rejeitam empresas perfeitamente válidas. Este post percorre as variantes país por país, os erros mais frequentes que vemos e uma implementação de referência open de como tratamos CUIT na Normadata.

Mod-11 não é um — é uma família

O esqueleto comum do módulo 11: você multiplica cada dígito do corpo por um peso de uma matriz, soma os produtos, calcula soma mod 11, e aplica uma regra para converter o resto no dígito verificador. O que muda entre países é: (a) quais dígitos formam o corpo, (b) qual matriz de pesos é usada, (c) o que fazer quando o resto é 10 (alguns países convertem em K, outros em 0, outros declaram o ID inválido), (d) se o algoritmo se aplica uma única vez ou duas vezes (CPF e CNPJ usam dois passos consecutivos), (e) se existem padrões uniformes que passam o algoritmo mas são inválidos por regra do registro. Tratar todos esses como variantes triviais do mesmo cálculo é a fonte da maioria dos bugs que vemos em código de produção.

Variantes principais país por país

Aqui vai a matriz concreta das seis variantes mod-11 mais comuns na LATAM, escritas como as usaríamos numa implementação:

  • CUIT (AR) — Corpo: 10 primeiros dígitos (prefixo de 2 + número de 8). Pesos: [5,4,3,2,7,6,5,4,3,2]. Regra: check = 11 - (soma mod 11). Se check = 11 → DV = 0. Se check = 10 → CUIT inválido com esse prefixo (a AFIP reemite com prefixo alternativo 23/24/33/34).
  • CPF (BR) — Dois DVs. Primeiro DV: corpo = 9 dígitos base, pesos = [10,9,8,7,6,5,4,3,2]. soma mod 11; se resto < 2 → DV = 0, caso contrário → DV = 11 - resto. Segundo DV: corpo = 10 dígitos (base + primeiro DV), pesos = [11,10,9,8,7,6,5,4,3,2]. Mesma regra. Além disso: os 10 padrões de todos os dígitos iguais (000.000.000-00 a 999.999.999-99) são inválidos por regra da Receita embora passem o algoritmo.
  • CNPJ (BR) — Dois DVs. Primeiro DV: corpo = 12 dígitos base, pesos = [5,4,3,2,9,8,7,6,5,4,3,2]. Note que os pesos não são monotonicamente descendentes — descem a 2 e depois saltam para 9. Segundo DV: corpo = 13 dígitos (base + primeiro DV), pesos = [6,5,4,3,2,9,8,7,6,5,4,3,2]. Mesma regra de all-equal que CPF.
  • RUT (CL) — Corpo: dígitos do RUT excluindo o DV. Pesos cíclicos [2,3,4,5,6,7] aplicados da direita. soma mod 11. Se resto = 11 → DV = 0. Se resto = 10 → DV = 'K' (maiúsculo). Outro resto → DV = 11 - resto.
  • NIT (CO) — Corpo: até 15 dígitos base (tipicamente 9). Pesos primos [3,7,13,17,19,23,29,37,41,43,47,53,59,67,71] aplicados da direita. Regra: se resto < 2 → DV = resto, caso contrário → DV = 11 - resto.
  • RUC (PE) — Corpo: 10 primeiros dígitos. Pesos: [5,4,3,2,7,6,5,4,3,2]. Mesmos pesos que CUIT mas semântica diferente (RUC começa com 10 para naturais ou 20 para jurídicas). soma mod 11; resto em {0,1} → DV = 0/1 conforme convenção SUNAT, outros → DV = 11 - resto.

Erros comuns em implementações caseiras

Estes são os bugs que mais vemos em code review de implementações internas de validação:

  • Esquecer que CPF e CNPJ rejeitam all-equal patterns — Um CPF como 111.111.111-11 passa o algoritmo mod-11 matematicamente, mas a Receita Federal nunca o emitiu e todo validador correto deve rejeitá-lo. Implementações caseiras frequentemente deixam passar porque só testam o algoritmo em abstrato.
  • Confundir K maiúsculo vs minúsculo em RUT — A convenção SII é K maiúsculo. Alguns sistemas armazenam k minúsculo, outros convertem para 10. Se você compara strings direto, 11.222.333-K vs 11.222.333-k vs 11.222.333-10 dão resultados diferentes. Sempre normalize para maiúsculo antes de comparar.
  • Aplicar o algoritmo de CUIT ao CUIL (ou vice-versa) — Mesmo algoritmo, semântica diferente. Um código que rejeita um CUIL por "não é CUIT" ou que valida qualquer CUIT como CUIL sem contexto está errado no momento errado. Ter um campo "tipo" separado do valor.
  • Calcular DV de CNPJ com pesos de CPF — Os pesos do primeiro DV de CNPJ são [5,4,3,2,9,8,7,6,5,4,3,2], não uma extensão monotônica dos pesos de CPF. Essa confusão é surpreendentemente frequente em code copiado de StackOverflow.
  • Não tratar o caso resto = 10 corretamente — Para CUIT, resto = 10 significa CUIT inválido com esse prefixo (não DV = 10). Para RUT, significa DV = 'K'. Tratar ambos os casos como DV = 10 numérico produz IDs impossíveis.
  • Validar com regex de comprimento sem checar DV — Uma regex que só verifica `^\d{11}$` para CPF rejeita CPFs com pontos válidos e aceita strings como 12345678901 que matematicamente não são válidas. A regex não substitui o cálculo.
  • Usar integers para o valor — JavaScript / Python tratam zeros à esquerda como significativos só em strings. Converter o ID para número perde zeros iniciais e quebra RUTs de 7 dígitos ou RUCs que começam com zero (raro mas existe em alguns países).
  • Não normalizar separadores antes de validar — Um CUIT pode chegar como 30-50001091-2, 30.50001091.2, 30 50001091 2, ou 30500010912. Todos são o mesmo identificador. Um strip de não-dígitos (exceto K para RUT) antes de calcular é obrigatório.
  • Ignorar a ordem de iteração dos pesos — Alguns algoritmos aplicam os pesos da esquerda para a direita (CUIT), outros da direita para a esquerda (NIT, RUT). Reverse do array vs iteração inversa são coisas diferentes e produzem DVs diferentes.

Como a Normadata trata todas as variantes

No catálogo interno (lib/catalog/validator-data.ts) cada país × identifier mapeia para um descriptor com seu algoritmo específico, sua matriz de pesos, suas regras de borda e seus padrões inválidos por regra do registro. O validador client-side (os tools em /tools) espelha exatamente a lógica do backend — não é uma aproximação, é a mesma função executada em JavaScript no browser. Isso significa que o mesmo input que o backend valida como inválido, o frontend marca como inválido instantaneamente, sem round-trip à API. A fonte de verdade é única.

Caveat estrutural: o DV não confirma registro

Um ponto que vale repetir: validar matematicamente um dígito verificador é validar estrutura. Não é validar que o identificador existe na AFIP, Receita Federal, SAT, SII, DIAN, SUNAT ou SENIAT. Um CPF estruturalmente válido pode ser de uma pessoa que não existe, de uma pessoa que já faleceu, ou de uma pessoa viva cujo CPF foi cancelado. Para confirmar registro ou estado ativo, você precisa de um serviço que efetivamente consulte o registro oficial — e esse serviço é independente da Normadata. O que você pode afirmar depois da validação da Normadata é que o identificador poderia existir: cumpre toda a estrutura matemática e semântica que um identificador real cumpre. Essa é a garantia explícita; a inversa (que existe) não podemos fazer sem tocar o registro.

Implementação de referência (TypeScript)

Aqui vai como fica a implementação de CUIT e CPF em TypeScript, escrita com clareza sobre concisão para servir como referência. A ideia é que se você vai implementar mod-11 caseiramente, ao menos use essas como ponto de partida e não algo baixado de StackOverflow sem entender os edge cases.

TypeScript — validação de CUIT (mod-11 com pesos [5,4,3,2,7,6,5,4,3,2])
const CUIT_WEIGHTS = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2] as const;
const VALID_PREFIXES = new Set(["20", "23", "24", "27", "30", "33", "34"]);

export function isValidCuit(input: string): boolean {
  const digits = input.replace(/\D/g, "");
  if (digits.length !== 11) return false;

  const prefix = digits.slice(0, 2);
  if (!VALID_PREFIXES.has(prefix)) return false;

  const body = digits.slice(0, 10).split("").map(Number);
  const dv = Number(digits[10]);

  let sum = 0;
  for (let i = 0; i < 10; i++) {
    sum += body[i] * CUIT_WEIGHTS[i];
  }

  const remainder = sum % 11;
  const check = 11 - remainder;

  if (check === 11) return dv === 0;
  if (check === 10) return false; // CUIT invalido com esse prefixo
  return dv === check;
}
TypeScript — validação de CPF (mod-11 duplo + rejeição de all-equal)
function calcCpfCheckDigit(digits: number[], weights: number[]): number {
  let sum = 0;
  for (let i = 0; i < digits.length; i++) {
    sum += digits[i] * weights[i];
  }
  const remainder = sum % 11;
  return remainder < 2 ? 0 : 11 - remainder;
}

export function isValidCpf(input: string): boolean {
  const digits = input.replace(/\D/g, "");
  if (digits.length !== 11) return false;

  // Regra Receita: rejeitar padroes com os 11 digitos iguais.
  if (/^(\d)\1{10}$/.test(digits)) return false;

  const nums = digits.split("").map(Number);
  const base = nums.slice(0, 9);

  const dv1 = calcCpfCheckDigit(base, [10, 9, 8, 7, 6, 5, 4, 3, 2]);
  if (dv1 !== nums[9]) return false;

  const base2 = nums.slice(0, 10);
  const dv2 = calcCpfCheckDigit(base2, [11, 10, 9, 8, 7, 6, 5, 4, 3, 2]);
  return dv2 === nums[10];
}
cURL — validar um CPF (inclui a rejeição do padrão all-equal)
curl -X POST https://api.normadata.io/v1/verify/tax-id \
  -H "X-API-Key: nd_your_key_here" \
  -H "Content-Type: application/json" \
  -d {
    "value": "111.111.111-11",
    "country": "BR",
    "type": "cpf"
  }
cURL — validar um CUIT com prefixo alternativo
curl -X POST https://api.normadata.io/v1/verify/tax-id \
  -H "X-API-Key: nd_your_key_here" \
  -H "Content-Type: application/json" \
  -d {
    "value": "23-25861053-4",
    "country": "AR",
    "type": "cuit"
  }

Conclusão

Se seu produto opera num único país da LATAM, escrever e manter uma implementação correta de mod-11 para esse país é totalmente factível — use as referências de cima como ponto de partida e teste contra casos conhecidos em produção. Se você opera em dois ou mais países, a pergunta não é se vale a pena escrever N implementações caseiras; é se vale a pena mantê-las todas em sincronia quando cada país publica edge cases novos. O stack típico — N variantes × M linguagens × bugs de borda — escala mal. A alternativa é delegar o algoritmo à Normadata via /v1/verify/tax-id (mesmo endpoint, todos os países) ou, se prefere ficar com OSS, basear-se nas bibliotecas canônicas mantidas (como python-stdnum para Python). Para a referência completa do endpoint, veja /docs/api/verify-tax-id. Para a cobertura por país e identificador, /coverage. Para a guia completa de tax IDs LATAM, /blog/latam-tax-ids-developer-guide.

Pronto para começar a desenvolver?

Solicitar acessoLer documentação

Artigos relacionados

5 de maio de 2026IDs Fiscais Latino-Americanos: Um Guia para Desenvolvedores5 de maio de 2026Validando o CUIT Argentino: Algoritmo, Formato e API5 de maio de 2026Validação de CPF: Algoritmo, Exemplos e API REST5 de maio de 2026RFC vs CURP no México: Quando Usar Cada Um15 de março de 2026Como Validar um Número de CUIT com uma API1 de abril de 2026Validação de CPF: Formato, Algoritmo e Integração com API para o Brasil2 de abril de 2026RFC no México: Formato, Estrutura e Validação para Desenvolvedores10 de março de 2026O Guia Completo de IDs Fiscais na América Latina1 de março de 2026Boas Práticas para Integrar APIs de Terceiros em Aplicações LATAM11 de maio de 2026Quanto orçamento KYC você desperdiça com dados malformados (e como medir)16 de maio de 2026Como validar todos os tax IDs da LATAM com uma única API16 de maio de 2026Por que pré-validar dados antes do KYC economiza dinheiro — com números16 de maio de 2026Construindo um formulário de checkout consciente da LATAM