CNPJ alfanumérico: o que muda no seu sistema
Checklist de migração para o CNPJ alfanumérico: coluna CHAR(14), collation, máscaras que removem letras, regex antigas, ERP, NF-e, bancos e testes.
Durante décadas, todo CNPJ teve 14 dígitos, e muitos sistemas foram escritos contando com isso: colunas numéricas, máscaras que apagam qualquer coisa que não seja dígito, regex com \d{14}, layouts de arquivo que preenchem o campo com zeros. Desde julho de 2026, a Receita Federal atribui CNPJs com letras às novas inscrições. Cada uma dessas premissas vira um ponto de falha, e a falha costuma aparecer longe de onde está o bug: um fornecedor que não consegue se cadastrar, uma nota que não é emitida, um boleto recusado. Este guia percorre as camadas de um sistema típico, do banco às integrações, com o que procurar e como corrigir.
O que mudou no CNPJ
A Instrução Normativa RFB nº 2.229/2024 alterou o formato do número de inscrição. Os pontos que importam para o código são poucos:
- O CNPJ continua com 14 posições: 8 de raiz, 4 de ordem do estabelecimento e 2 dígitos verificadores.
- As 12 primeiras posições aceitam letras de A a Z e dígitos de 0 a 9.
- Os dois dígitos verificadores continuam numéricos.
- No cálculo, cada caractere vale o seu código ASCII menos 48. Para dígitos, isso dá o próprio dígito;
Avale 17,Bvale 18 e assim por diante atéZ, que vale 42. Pesos e regra do módulo 11 não mudaram. - O formato alfanumérico é atribuído apenas a novas inscrições. Os CNPJs numéricos existentes não mudam.
A consequência prática é que o seu sistema vai conviver com os dois formatos indefinidamente. 11.222.333/0001-81 e 12.ABC.345/01DE-35 são ambos válidos, e o código precisa aceitar os dois sem distinguir caminho. A estrutura do CNPJ detalha o cálculo, e o guia de cronograma e regras resume o que a Receita publicou.
Checklist por camada
A tabela resume o que costuma estar errado e o que fazer em cada camada. As seções seguintes explicam cada linha.
| Camada | Antes (só numérico) | Depois (numérico e alfanumérico) |
|---|---|---|
| Banco | BIGINT, NUMERIC(14) ou VARCHAR sem regra |
CHAR(14) só com maiúsculas, CHECK de formato e de dígitos |
| API | Esquema com type: integer ou padrão ^\d{14}$ |
type: string com padrão ^[A-Z0-9]{12}[0-9]{2}$ |
| Front-end | replace(/\D/g, ''), inputmode="numeric" |
toUpperCase() e replace(/[^A-Z0-9]/g, ''), teclado de texto |
| ERP | Picture e validação só com dígitos | Validação com ASCII − 48 e conversão para maiúsculas |
| Fiscal | Campo de 14 dígitos no emissor | Campo de 14 posições com letras, conforme o layout vigente |
| Integrações | Campos numéricos com zeros à esquerda | Campos alfanuméricos, confirmados com cada parceiro |
A linha da API merece atenção especial, porque é a que mais afeta quem você não controla. Se o contrato publicado declara o CNPJ como número, ou como texto com o padrão ^\d{14}$, cada consumidor gerou código a partir dele, e corrigir só o servidor não basta. Mude o tipo para texto com o padrão alfanumérico, avise os consumidores com antecedência e, se a API é versionada, decida se a mudança justifica uma versão nova: para quem valida a resposta contra o esquema antigo, um CNPJ com letras é uma quebra de contrato. Nas respostas, devolva sempre o mesmo formato, de preferência os 14 caracteres em maiúsculas sem pontuação, e deixe a máscara para quem exibe. Nas entradas, aceite com e sem pontuação e em minúsculas, normalize e só então valide. Essa assimetria, tolerante na entrada e estrita na saída, reduz o número de integrações que quebram no primeiro CNPJ alfanumérico.
Um bom primeiro passo é procurar no código-fonte os sinais típicos: \D, \d{14}, [0-9]{14}, parseInt, Number(, BIGINT, isdigit, IsNumeric, Val( e inputmode="numeric" perto de qualquer coisa chamada cnpj. A busca não encontra tudo, mas acha a maior parte dos problemas em poucos minutos.
Banco de dados: tipos e índices
Use CHAR(14), nunca BIGINT ou NUMERIC. Um tipo numérico já era uma má escolha para o CNPJ numérico, porque descarta zeros à esquerda: um CNPJ cuja raiz começa com 0 volta do banco com 13 dígitos. Com o alfanumérico, ele simplesmente não comporta o valor. Guarde as 14 posições sem pontuação, em maiúsculas, e deixe a máscara para a camada de apresentação.
Cuidado com a collation. No PostgreSQL, as collations padrão são determinísticas e diferenciam maiúsculas de minúsculas: 12abc34501de35 e 12ABC34501DE35 seriam duas linhas diferentes num índice único. A solução é normalizar para maiúsculas antes de gravar e recusar minúsculas na restrição. Resista à tentação de usar citext para “resolver” o índice: com citext, os operadores de regex também passam a ignorar maiúsculas e minúsculas, e uma CHECK com ~ '^[A-Z0-9]...' aceitaria minúsculas sem avisar. No MySQL, o padrão é o contrário: collations como utf8mb4_0900_ai_ci ignoram maiúsculas e acentos, tanto no índice quanto no REGEXP. Declarar a coluna como CHAR(14) CHARACTER SET ascii COLLATE ascii_bin torna o comportamento previsível. No SQL Server, verifique a collation do banco, que muitas vezes é insensível a maiúsculas.
A função abaixo, em PL/pgSQL, segue a mesma regra do validador de CNPJ: remove ponto, barra, hífen e espaços, converte para maiúsculas, exige 12 posições alfanuméricas e 2 dígitos, recusa sequências repetidas e confere os verificadores com ascii(c) - 48.
CREATE OR REPLACE FUNCTION cnpj_valido(entrada text)
RETURNS boolean
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
d text;
pesos1 int[] := ARRAY[5,4,3,2,9,8,7,6,5,4,3,2];
pesos2 int[] := ARRAY[6,5,4,3,2,9,8,7,6,5,4,3,2];
soma int;
resto int;
dv1 int;
dv2 int;
BEGIN
IF entrada IS NULL THEN
RETURN NULL;
END IF;
d := upper(regexp_replace(entrada, '[./[:space:]-]', '', 'g'));
IF d !~ '^[0-9A-Z]{12}[0-9]{2}$' THEN
RETURN false;
END IF;
-- 12 caracteres iguais passam na conta, mas não são considerados válidos
IF substr(d, 1, 12) = repeat(substr(d, 1, 1), 12) THEN
RETURN false;
END IF;
soma := 0;
FOR i IN 1..12 LOOP
soma := soma + (ascii(substr(d, i, 1)) - 48) * pesos1[i];
END LOOP;
resto := soma % 11;
dv1 := CASE WHEN resto < 2 THEN 0 ELSE 11 - resto END;
IF substr(d, 13, 1)::int <> dv1 THEN
RETURN false;
END IF;
soma := 0;
FOR i IN 1..13 LOOP
soma := soma + (ascii(substr(d, i, 1)) - 48) * pesos2[i];
END LOOP;
resto := soma % 11;
dv2 := CASE WHEN resto < 2 THEN 0 ELSE 11 - resto END;
RETURN substr(d, 14, 1)::int = dv2;
END;
$$;
SELECT cnpj_valido('11.222.333/0001-81'); -- true
SELECT cnpj_valido('12.ABC.345/01DE-35'); -- true
SELECT cnpj_valido('11.222.333/0001-82'); -- false (segundo dígito)
SELECT cnpj_valido('00.000.000/0000-00'); -- false (repetido)Se a coluna atual é numérica, a migração tem duas etapas: trocar o tipo, recuperando os zeros à esquerda, e aplicar a restrição sem uma varredura longa da tabela. No PostgreSQL, NOT VALID faz a restrição valer para escritas novas imediatamente, sem verificar as linhas antigas; VALIDATE CONSTRAINT confere o histórico depois, com um bloqueio que não impede leituras nem escritas.
-- 1. Troca o tipo (reescreve a tabela; planeje uma janela)
ALTER TABLE fornecedores
ALTER COLUMN cnpj TYPE char(14)
USING lpad(cnpj::text, 14, '0');
-- 2. A regra passa a valer para escritas novas
ALTER TABLE fornecedores
ADD CONSTRAINT fornecedores_cnpj_valido
CHECK (cnpj ~ '^[0-9A-Z]{12}[0-9]{2}$' AND cnpj_valido(cnpj)) NOT VALID;
-- 3. Lista o que não passa, para tratar antes de validar
SELECT id, cnpj FROM fornecedores
WHERE NOT (cnpj ~ '^[0-9A-Z]{12}[0-9]{2}$' AND cnpj_valido(cnpj));
-- 4. Confere as linhas antigas
ALTER TABLE fornecedores VALIDATE CONSTRAINT fornecedores_cnpj_valido;A regex fica na CHECK porque a função aceita pontuação e minúsculas, e a coluna não deve aceitar. O guia de validação de CPF no banco de dados discute as mesmas técnicas em mais detalhe, incluindo a alternativa com triggers no MySQL, que não permite chamar funções armazenadas dentro de uma CHECK.
Front-end: máscaras que removem letras
O bug mais comum está numa linha que parece inofensiva:
// Antes: remove tudo que não é dígito
const limpo = valor.replace(/\D/g, '');
// '12.ABC.345/01DE-35' vira '123450135': 9 caracteres, sem as letras
// O usuário digita um CNPJ válido e vê "CNPJ inválido"A correção é trocar a classe de caracteres e converter para maiúsculas antes de limpar:
function limparCnpj(valor) {
return String(valor).toUpperCase().replace(/[^A-Z0-9]/g, '');
}
function mascaraCnpj(valor) {
const c = limparCnpj(valor).slice(0, 14);
let out = c.slice(0, 2);
if (c.length > 2) out += '.' + c.slice(2, 5);
if (c.length > 5) out += '.' + c.slice(5, 8);
if (c.length > 8) out += '/' + c.slice(8, 12);
if (c.length > 12) out += '-' + c.slice(12, 14);
return out;
}
mascaraCnpj('12abc34501de35'); // '12.ABC.345/01DE-35'
mascaraCnpj('11222333000181'); // '11.222.333/0001-81'Revise também os atributos do campo. inputmode="numeric" abre o teclado numérico no celular, e o usuário fica sem como digitar letras; use o teclado de texto com autocapitalize="characters". Bibliotecas de máscara configuradas com 00.000.000/0000-00 ou 99.999.999/9999-99 costumam aceitar só dígitos nessas posições; confira na documentação da sua qual símbolo representa “letra ou dígito”. O formatador de CNPJ mostra o comportamento esperado, e o guia de máscara de CPF em React, Vue e Angular cobre a parte de cursor e colagem, que é igual.
Regex antigas
Qualquer regex com \d nas 12 primeiras posições recusa o formato novo. As trocas são diretas:
| Antes | Depois |
|---|---|
^\d{14}$ |
^[A-Z0-9]{12}\d{2}$ |
^\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}$ |
^[A-Z0-9]{2}\.[A-Z0-9]{3}\.[A-Z0-9]{3}\/[A-Z0-9]{4}-\d{2}$ |
Repare que os dois últimos caracteres continuam \d: letra no dígito verificador é sempre erro. Procure regex também fora do código-fonte: em esquemas JSON e OpenAPI, em validações de formulário configuradas no banco, em planilhas de importação e em ferramentas de ETL. O guia de regex de CNPJ traz as versões para HTML, Google Forms e regex combinada de CPF ou CNPJ.
ERP e fiscal: Protheus/ADVPL, Delphi, NF-e e SPED
Protheus/ADVPL. Em geral, campos de CNPJ do dicionário já são do tipo caractere com 14 posições, então o armazenamento costuma estar pronto. O risco está em volta: pictures como @R 99.999.999/9999-99, em que o 9 só admite dígitos; validações customizadas que usam Val(), IsDigit() ou comparam com "0" a "9"; e gatilhos que limpam o campo com StrTran só para dígitos. Verifique se a versão do seu Protheus já traz as validações padrão atualizadas e revise cada customização própria.
Delphi. Rotinas antigas costumam usar StrToInt ou StrToInt64 nos caracteres. Com letras, isso gera exceção. A regra nova cabe em Ord(c) - 48:
function DigitoCnpj(const S: string; const Pesos: array of Integer): Integer;
var
I, Soma, Resto: Integer;
begin
Soma := 0;
for I := 1 to Length(S) do
Soma := Soma + (Ord(S[I]) - 48) * Pesos[I - 1];
Resto := Soma mod 11;
if Resto < 2 then Result := 0 else Result := 11 - Resto;
end;
function CnpjValido(const Valor: string): Boolean;
const
P1: array[0..11] of Integer = (5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2);
P2: array[0..12] of Integer = (6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2);
var
C: string;
I, D1, D2: Integer;
Repetido: Boolean;
begin
Result := False;
C := UpperCase(Valor); // só A-Z: não converte acentos
C := StringReplace(C, '.', '', [rfReplaceAll]);
C := StringReplace(C, '/', '', [rfReplaceAll]);
C := StringReplace(C, '-', '', [rfReplaceAll]);
C := StringReplace(C, ' ', '', [rfReplaceAll]);
if Length(C) <> 14 then Exit;
for I := 1 to 12 do
if not CharInSet(C[I], ['0'..'9', 'A'..'Z']) then Exit;
if not CharInSet(C[13], ['0'..'9']) or not CharInSet(C[14], ['0'..'9']) then Exit;
Repetido := True;
for I := 2 to 12 do
if C[I] <> C[1] then Repetido := False;
if Repetido then Exit;
D1 := DigitoCnpj(Copy(C, 1, 12), P1);
D2 := DigitoCnpj(Copy(C, 1, 12) + IntToStr(D1), P2);
Result := (Ord(C[13]) - 48 = D1) and (Ord(C[14]) - 48 = D2);
end;UpperCase só converte letras ASCII, o que é o desejado aqui: um ç continua ç e é recusado. Versões completas, com testes, ficam em CNPJ em Delphi e CNPJ em Excel/VBA.
NF-e e SPED. Nos documentos fiscais, o CNPJ continua ocupando 14 posições; o que muda é que ele pode conter letras. Consulte as notas técnicas vigentes do Portal Nacional da NF-e e os leiautes do SPED sobre o CNPJ alfanumérico. Como os detalhes dependem da versão do schema e do programa validador, verifique a versão do layout do seu emissor e do seu gerador de arquivos antes de emitir com um CNPJ alfanumérico. Preste atenção especial à chave de acesso da NF-e, que inclui o CNPJ do emitente: rotinas que a tratam como um número de 44 dígitos precisam ser revistas.
Integrações bancárias e boletos
Esta é a camada que você menos controla, por isso a recomendação é perguntar cedo. Em layouts posicionais, como os arquivos de remessa e retorno de cobrança, campos de inscrição costumam ser definidos como numéricos e preenchidos com zeros à esquerda. Um CNPJ alfanumérico nesse campo pode ser recusado pelo banco ou, pior, truncado em silêncio pelo seu gerador de arquivos.
Recomendações práticas:
- Confirme com cada banco a versão de layout que aceita letras no CNPJ do beneficiário e do pagador.
- Revise o gerador de arquivos: funções que formatam o campo com
padStartde zeros funcionam, mas as que convertem para número antes não. - Teste a leitura do retorno, não só o envio: o parser precisa aceitar letras na mesma posição.
- Inclua chaves Pix do tipo CNPJ, APIs de consulta e conciliação na mesma revisão.
- Para cada parceiro sem resposta, registre o risco e um plano B, como cadastro manual temporário.
Testes de regressão
Uma lista curta de casos cobre a maior parte das regressões. Rode-a em cada camada: na função de validação, na API, no front-end e, se possível, num teste de ponta a ponta que grave e leia do banco.
| Entrada | Esperado | O que testa |
|---|---|---|
11.222.333/0001-81 |
válido | CNPJ numérico continua funcionando |
11222333000181 |
válido | entrada sem pontuação |
12.ABC.345/01DE-35 |
válido | letras na raiz e na ordem |
12.abc.345/01de-35 |
válido após normalizar | minúsculas viram maiúsculas |
11.222.333/0001-82 |
inválido (dv2) | segundo dígito verificador |
11.222.333/0001-91 |
inválido (dv1) | primeiro dígito verificador |
00.000.000/0000-00 |
inválido (repetido) | sequência repetida |
1.222.333/0001-81 |
inválido (tamanho) | 13 caracteres |
12.ABC.345/01DE-3X |
inválido (caracteres) | letra no dígito verificador |
Rode a lista inteira antes de mexer em qualquer camada, para registrar o que já falha, e de novo depois de cada mudança. É comum descobrir que a validação foi corrigida, mas o CNPJ alfanumérico ainda é recusado por uma regex esquecida num esquema de API ou cortado por uma coluna de relatório.
Acrescente casos de ida e volta: gravar 12abc34501de35 e ler 12ABC34501DE35; exibir 12.ABC.345/01DE-35 com a máscara; tentar inserir o mesmo CNPJ em minúsculas e em maiúsculas e conferir que o índice único recusa o segundo. Para massa de teste, use os exemplos de CNPJ alfanumérico para teste ou o gerador de CNPJ em lote. Os números gerados são fictícios e podem coincidir com um CNPJ real por acaso; não os use fora de ambientes de teste.
Perguntas frequentes
Preciso migrar os CNPJs numéricos que já estão no banco?
Os números não mudam: quem já tem CNPJ continua com o mesmo. O que pode precisar de migração é o tipo da coluna. Se ela for BIGINT ou NUMERIC, converta para CHAR(14), completando os zeros à esquerda que o tipo numérico descartou.
Posso guardar o CNPJ em minúsculas?
Não é recomendado. Normalize para maiúsculas na entrada e guarde só maiúsculas. Assim o índice único funciona com qualquer collation e a comparação entre sistemas não depende de configuração.
O cálculo dos dígitos verificadores mudou?
Os pesos e a regra do módulo 11 continuam os mesmos. A diferença é o valor de cada caractere, que passa a ser o código ASCII menos 48. Para dígitos, isso dá o próprio dígito, então CNPJs numéricos continuam válidos com o código novo.