Como validar todos os tax IDs da LATAM com uma única API
Se seu produto opera em mais de um país da LATAM, eventualmente você vai esbarrar no mesmo problema: cada identificador tributário tem seu próprio formato, sua própria autoridade emissora e seu próprio algoritmo de dígito verificador. O típico é terminar com uma biblioteca diferente por país, multiplicada por cada linguagem do seu stack: uma para CPF em Node, outra equivalente em Python para o batch job, uma terceira em Go para o microserviço de pagamentos. Cada uma é mantida em um ritmo diferente, cada uma tem seus próprios bugs e cada uma expressa o resultado de forma diferente. Este post mostra como unificar os 17 identificadores dos 10 países ao vivo na América do Sul atrás de um único endpoint REST da Normadata. Vamos percorrer a tabela por país, os algoritmos resumidos, exemplos em cURL, TypeScript e Python, performance esperada e como é a migração de bibliotecas open-source.
O problema: N bibliotecas × M linguagens
Um time típico que cobre Argentina, Brasil e Chile termina com um combo parecido com este: `cpf-cnpj-validator` e `validador-cuit` em Node, `python-stdnum` para o batch job noturno, uma regex caseira para RUT chileno porque não acharam uma biblioteca confiável, e um script em Go que reimplementa CPF na mão porque a biblioteca oficial está abandonada há dois anos. Cada uma usa um nome diferente para o resultado (`isValid`, `valid`, `ok`, `validate()`), cada uma tem comportamento diferente diante de inputs com lixo (algumas lançam exceção, outras retornam `false`, outras retornam `null`), e cada uma é atualizada num ciclo de release diferente. Quando você adiciona Colômbia, México e Peru, a matriz fica ingovernável. A ideia da Normadata é simples: um endpoint, uma forma de request, uma forma de response, todos os países.
Os 10 países ao vivo e seus identificadores
A cobertura atual cobre 10 países sul-americanos ao vivo. Esta é a lista de identificadores principais por país:
- Argentina (AR) — CUIT, CUIL, DNI, CBU, CVU. CUIT/CUIL: 11 dígitos com dígito verificador módulo 11. DNI: 7-8 dígitos sem DV, validado por comprimento e range. CBU: 22 dígitos com dois DVs por bloco. CVU: 22 dígitos seguindo a estrutura de CBU emitida por PSPs.
- Brasil (BR) — CPF, CNPJ, IBAN (quando aplicável). CPF: 11 dígitos com DV módulo 11 em dois passos. CNPJ: 14 dígitos com DV módulo 11 em dois passos.
- Chile (CL) — RUT, RUN. 7-9 dígitos + char verificador (0-9 ou K) por módulo 11.
- Colômbia (CO) — NIT, Cédula. NIT: 9 dígitos + DV módulo 11 com pesos primos.
- Uruguai (UY) — RUT, Cédula. RUT: 12 dígitos com DV.
- Paraguai (PY) — RUC. 6-8 dígitos + DV módulo 11.
- Peru (PE) — RUC, DNI, CUI. RUC: 11 dígitos começando com 10 (natural) ou 20 (jurídica), DV módulo 11.
- Equador (EC) — RUC, Cédula CI. CI: 10 dígitos com DV módulo 10.
- Bolívia (BO) — NIT. 7-12 dígitos.
- Venezuela (VE) — RIF, Cédula. RIF: prefixo V/E/J/G/C + 9 dígitos + DV.
Algoritmos resumidos
Embora a família de algoritmos seja similar (a maioria são variantes de módulo 11), cada país tem sua própria matriz de pesos e regras de borda. O importante é não confundi-los. A seguir, as variantes principais:
- CUIT/CUIL (AR) — Módulo 11 ponderado com pesos [5,4,3,2,7,6,5,4,3,2] sobre os 10 primeiros dígitos. Se o resultado é 10, o CUIT é inválido com esse prefixo (a AFIP reemite com um prefixo alternativo). Se é 11, o DV é 0.
- CPF (BR) — Módulo 11 duplo. Primeiro DV: pesos [10,9,8,7,6,5,4,3,2] sobre os 9 dígitos base. Segundo DV: pesos [11,10,9,8,7,6,5,4,3,2] sobre os 10 dígitos (base + primeiro DV). Resto < 2 → DV = 0; caso contrário, DV = 11 - resto. Além disso: CPFs com os 11 dígitos iguais (000.000.000-00 a 999.999.999-99) são inválidos por regra da Receita Federal embora passem o algoritmo.
- CNPJ (BR) — Módulo 11 duplo com pesos [5,4,3,2,9,8,7,6,5,4,3,2] e [6,5,4,3,2,9,8,7,6,5,4,3,2]. Mesma regra de all-equal.
- RUT (CL) — Módulo 11 com pesos cíclicos [2,3,4,5,6,7] da direita. Se resto = 11 → DV = 0. Se resto = 10 → DV = K (maiúsculo). Outro resto → DV = 11 - resto.
- NIT (CO) — Módulo 11 com pesos primos [3,7,13,17,19,23,29,37,41,43,47,53,59,67,71] aplicados da direita sobre os 9 dígitos base.
- RUC (PE) — Módulo 11 com pesos [5,4,3,2,7,6,5,4,3,2] sobre os 10 primeiros dígitos (mesmos pesos que CUIT, semântica diferente).
- RIF (VE) — Módulo 11 com pesos específicos do SENIAT e um valor inicial dependente da letra de tipo (V/E/J/G/C).
- Cédula EC (CI) — Módulo 10 (algoritmo diferente dos anteriores) com pesos [2,1,2,1,2,1,2,1,2] sobre os 9 primeiros dígitos.
- CBU (AR) — Dois dígitos verificadores, um por bloco (8 dígitos do banco/agência, 14 dígitos da conta), cada um com sua própria matriz de pesos.
Como a Normadata unifica tudo: um endpoint, um envelope
O endpoint canônico é POST /v1/verify/tax-id. Recebe três campos no body: country (código ISO de 2 letras), type (slug do identificador) e value (a string a validar, com ou sem separadores). Devolve um envelope plano: valid, country, type, value.raw, value.formatted e um metadata específico do identificador. Auth é X-API-Key com prefixo nd_. Vamos ver os mesmos três países que mencionamos no começo:
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": "30-50001091-2",
"country": "AR",
"type": "cuit"
}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.444.777-35",
"country": "BR",
"type": "cpf"
}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": "12.345.678-5",
"country": "CL",
"type": "rut"
}type TaxIdInput = { country: string; type: string; value: string };
type TaxIdResult = {
valid: boolean;
country: string;
type: string;
value: { raw: string; formatted: string };
metadata?: Record<string, unknown>;
};
async function verifyTaxId(input: TaxIdInput): Promise<TaxIdResult> {
const res = await fetch("https://api.normadata.io/v1/verify/tax-id", {
method: "POST",
headers: {
"X-API-Key": process.env.NORMADATA_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(input),
});
if (!res.ok) {
throw new Error(`Normadata error: ${res.status}`);
}
return res.json();
}const inputs: TaxIdInput[] = [
{ country: "AR", type: "cuit", value: "30-50001091-2" },
{ country: "BR", type: "cpf", value: "111.444.777-35" },
{ country: "BR", type: "cnpj", value: "11.222.333/0001-81" },
{ country: "CL", type: "rut", value: "12.345.678-5" },
{ country: "CO", type: "nit", value: "900123456-7" },
{ country: "PE", type: "ruc", value: "20100070970" },
];
const results = await Promise.all(inputs.map(verifyTaxId));
for (const r of results) {
console.log(r.country, r.type, r.valid, r.value.formatted);
}import os
import requests
API_KEY = os.environ["NORMADATA_API_KEY"]
URL = "https://api.normadata.io/v1/verify/tax-id"
def verify_tax_id(country: str, id_type: str, value: str) -> dict:
response = requests.post(
URL,
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
json={"country": country, "type": id_type, "value": value},
timeout=2.0,
)
response.raise_for_status()
return response.json()
if __name__ == "__main__":
print(verify_tax_id("AR", "cuit", "30-50001091-2"))
print(verify_tax_id("BR", "cpf", "111.444.777-35"))
print(verify_tax_id("CL", "rut", "12.345.678-5"))Performance esperada
O endpoint de validação de tax IDs serve respostas em menos de 30 ms p95 medido a partir de São Paulo. A validação é matemática pura — não há consulta a registros, não há round-trip para AFIP/Receita/SAT/SII — então a latência depende quase inteiramente da rede e da região do cliente. Se seu app vive na mesma região AWS que a API, é razoável adicionar entre 1 e 5 ms pela desserialização JSON e nada mais. Para batch jobs ou fluxos mobile-first onde a latência importa menos, a disponibilidade é o que move a agulha: uma API caída do lado do provider para um país específico não para os outros 9, porque tudo roda atrás do mesmo endpoint.
Migração de bibliotecas OSS
Se você já tem bibliotecas OSS instaladas, a migração raramente é um big-bang. O padrão típico é introduzir a Normadata como uma camada de abstração e migrar país por país conforme as bibliotecas OSS dão problema. O que muda no seu code:
- Uma única dependência HTTP — Em vez de ter `cpf-cnpj-validator`, `validador-cuit`, `rut-helpers` e um script caseiro de RUC no seu package.json, seu código só precisa de fetch (ou requests, ou net/http). As bibliotecas podem ser retiradas gradualmente.
- Forma de resposta unificada — Em vez de mapear `lib.isValid()` vs `lib.validate().ok` vs `lib.check() === true`, todas as chamadas devolvem o mesmo envelope com `valid: boolean`. Sua camada de aplicação deixa de ter if/else por biblioteca.
- Normalização integrada — As bibliotecas OSS raramente normalizam o output. A Normadata devolve `value.formatted` com o formato canônico (pontos para CPF, hífens para CUIT, K maiúsculo para RUT) que você pode salvar direto no banco.
- Metadata estruturado — `entity_type`, `prefix`, `check_digit`, `region_code` são devolvidos em `metadata`. Sem parsear strings você mesmo.
- Caching local — Como o envelope é estável, você pode cachear localmente por `country:type:value` com um TTL razoável (24h ou mais) se se preocupa com round-trips. A validação é idempotente.
- Tratamento de erros HTTP — Os erros viram 422 (input inválido por formato), 400 (request malformado), 429 (rate limit) e 5xx (Normadata-side). Uma única tabela de mapeamento em vez de N tipos de exceção.
Próximos passos
Se você já mantém mais de duas bibliotecas de validação de tax IDs, vale ao menos prototipar a substituição. Para começar, leia a referência do endpoint em /docs/api/verify-tax-id e a cobertura completa por país em /coverage. Para acesso à API durante o período de acesso antecipado, solicite uma key em /waitlist. Se seu caso de uso específico é pré-validação antes do KYC, leia também /blog/kyc-budget-malformed-data.