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

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 — validar um CUIT argentino
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 — validar um CPF brasileiro
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 — validar um RUT chileno
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"
  }
TypeScript — cliente mínimo reutilizável (fetch nativo)
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();
}
TypeScript — validar vários países em paralelo
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);
}
Python — cliente síncrono com requests
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.

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 2026Por que pré-validar dados antes do KYC economiza dinheiro — com números16 de maio de 2026Construindo um formulário de checkout consciente da LATAM16 de maio de 2026O custo oculto dos erros de mod-11 no seu onboarding LATAM