Alphanumeric CNPJ: validate the check digits and look it up via API
Since July 31, 2026, Receita Federal has been issuing CNPJs with letters and numbers. The first one was a Banco do Brasil branch, 00.000.000/E08G-12. The new format is being rolled out gradually during 2026: new CNPJs may still be numeric only, and existing CNPJs remain valid, with no replacement. Sources: Receita Federal news release and Q&A on the alphanumeric CNPJ (in Portuguese).
For integrations, what changes is validation: a system that treats the CNPJ as a number, or that only accepts digits in the field, starts rejecting real companies. This guide covers the format, the regular expression, the check digit calculation with ready-to-use code, the database changes and the lookup through the CNPJAPI API, which already accepts the new format.
What changes in the format
The CNPJ still has 14 characters and the same mask. The difference is that the first 12 positions can contain uppercase letters, from A to Z:
| Positions | What it is | Can contain letters? |
|---|---|---|
| 1 to 8 | Root (identifies the company) | Yes |
| 9 to 12 | Branch number (head office or branch) | Yes |
| 13 and 14 | Check digits | No, always numbers |
With the mask, the sample 12ABC34501DE35 becomes 12.ABC.345/01DE-35.
Regular expression
To check only the format, before calculating the check digits:
- Without the mask:
^[A-Z0-9]{12}[0-9]{2}$ - With the mask:
^[A-Z0-9]{2}\.[A-Z0-9]{3}\.[A-Z0-9]{3}\/[A-Z0-9]{4}-[0-9]{2}$
Both accept uppercase only: convert the input first or replace [A-Z0-9] with [A-Za-z0-9]. And a regular expression alone is not enough, because it accepts any 14-character combination in the right format. Always validate the check digits.
How to calculate the check digits
The calculation is the same modulo 11 as always. The only change is the value of each character: it is worth its ASCII code minus 48. For numbers, that gives the number itself (0 is 0, 9 is 9). For letters, A is 17, B is 18, and so on up to Z, which is 42. That is why a numeric-only CNPJ keeps the same check digits as before.
- Multiply each of the first 12 characters by a weight. Weights go from 2 to 9, right to left, and start over at 2 after 9. Left to right, they are 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2.
- Add up the results and take the remainder of the division by 11.
- If the remainder is 0 or 1, the digit is 0. Otherwise, the digit is 11 minus the remainder.
- For the second digit, repeat with the first 13 characters (the 12 plus the first check digit), with weights 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2.
For the sample 12ABC34501DE, the sum is 459, the remainder is 8 and the first digit is 3. With 12ABC34501DE3, the sum is 424, the remainder is 6 and the second digit is 5: 12ABC34501DE35.
Every version below accepts the CNPJ with or without the mask and in lowercase, rejects characters outside A to Z and 0 to 9 (including accented letters and other Unicode characters), rejects repeated sequences (such as 00000000000000) and validates both digits.
Python
import re
def calcular_dv(base: str) -> str:
soma, peso = 0, 2
for c in reversed(base):
soma += (ord(c) - 48) * peso
peso = 2 if peso == 9 else peso + 1
resto = soma % 11
return "0" if resto < 2 else str(11 - resto)
def cnpj_valido(entrada: str) -> bool:
cnpj = re.sub(r"[.\-/\s]", "", entrada, flags=re.ASCII)
if not re.fullmatch(r"[A-Za-z0-9]{12}[0-9]{2}", cnpj):
return False
cnpj = cnpj.upper()
if len(set(cnpj)) == 1:
return False
return calcular_dv(cnpj[:12]) == cnpj[12] and calcular_dv(cnpj[:13]) == cnpj[13]
print(cnpj_valido("12.ABC.345/01DE-35")) # True
To generate a test CNPJ, pick 12 random characters from A to Z and 0 to 9 and append the two digits: base + calcular_dv(base) gives the first 13, and calcular_dv of those 13 gives the last one.
JavaScript (Node.js and browser)
function calcularDv(base) {
let soma = 0;
let peso = 2;
for (let i = base.length - 1; i >= 0; i--) {
soma += (base.charCodeAt(i) - 48) * peso;
peso = peso === 9 ? 2 : peso + 1;
}
const resto = soma % 11;
return resto < 2 ? "0" : String(11 - resto);
}
function cnpjValido(entrada) {
let cnpj = entrada.replace(/[.\-/ \t\r\n]/g, "");
if (!/^[A-Za-z0-9]{12}[0-9]{2}$/.test(cnpj)) return false;
cnpj = cnpj.toUpperCase();
if (new Set(cnpj).size === 1) return false;
return calcularDv(cnpj.slice(0, 12)) === cnpj[12] && calcularDv(cnpj.slice(0, 13)) === cnpj[13];
}
PHP
function calcularDv(string $base): string
{
$soma = 0;
$peso = 2;
for ($i = strlen($base) - 1; $i >= 0; $i--) {
$soma += (ord($base[$i]) - 48) * $peso;
$peso = $peso === 9 ? 2 : $peso + 1;
}
$resto = $soma % 11;
return $resto < 2 ? '0' : (string) (11 - $resto);
}
function cnpjValido(string $entrada): bool
{
$cnpj = preg_replace('/[.\-\/\s]/', '', $entrada);
if (!preg_match('/^[A-Za-z0-9]{12}[0-9]{2}$/', $cnpj)) {
return false;
}
$cnpj = strtoupper($cnpj);
if (count(array_unique(str_split($cnpj))) === 1) {
return false;
}
return calcularDv(substr($cnpj, 0, 12)) === $cnpj[12] && calcularDv(substr($cnpj, 0, 13)) === $cnpj[13];
}
C# / .NET
using System.Linq;
using System.Text.RegularExpressions;
public static class Cnpj
{
public static char CalcularDv(string baseCnpj)
{
int soma = 0, peso = 2;
for (var i = baseCnpj.Length - 1; i >= 0; i--)
{
soma += (baseCnpj[i] - 48) * peso;
peso = peso == 9 ? 2 : peso + 1;
}
var resto = soma % 11;
return resto < 2 ? '0' : (char)('0' + (11 - resto));
}
public static bool Valido(string entrada)
{
var cnpj = Regex.Replace(entrada, @"[.\-/ \t\r\n]", "");
if (!Regex.IsMatch(cnpj, @"^[A-Za-z0-9]{12}[0-9]{2}\z"))
return false;
cnpj = cnpj.ToUpperInvariant();
if (cnpj.Distinct().Count() == 1)
return false;
return CalcularDv(cnpj[..12]) == cnpj[12] && CalcularDv(cnpj[..13]) == cnpj[13];
}
}
Go
package cnpj
import (
"regexp"
"strconv"
"strings"
)
var (
separadores = regexp.MustCompile(`[.\-/\s]`)
formato = regexp.MustCompile(`^[A-Za-z0-9]{12}[0-9]{2}$`)
)
func calcularDV(base string) byte {
soma, peso := 0, 2
for i := len(base) - 1; i >= 0; i-- {
soma += int(base[i]-48) * peso
if peso == 9 {
peso = 2
} else {
peso++
}
}
resto := soma % 11
if resto < 2 {
return '0'
}
return strconv.Itoa(11 - resto)[0]
}
func Valido(entrada string) bool {
cnpj := separadores.ReplaceAllString(entrada, "")
if !formato.MatchString(cnpj) {
return false
}
cnpj = strings.ToUpper(cnpj)
if strings.Count(cnpj, cnpj[:1]) == len(cnpj) {
return false
}
return calcularDV(cnpj[:12]) == cnpj[12] && calcularDV(cnpj[:13]) == cnpj[13]
}
Database and screens
- Text column: store the CNPJ as
CHAR(14)orVARCHAR(14), without the mask. Numeric columns (BIGINT,NUMERIC) cannot hold letters and already dropped leading zeros. To migrate a numeric column:
-- PostgreSQL: from BIGINT to text, restoring the leading zeros
ALTER TABLE empresa ALTER COLUMN cnpj TYPE CHAR(14) USING LPAD(cnpj::text, 14, '0');
-- MySQL
ALTER TABLE empresa MODIFY cnpj CHAR(14);
UPDATE empresa SET cnpj = LPAD(cnpj, 14, '0');
- With the mask: if your system stores the formatted CNPJ, the column needs 18 characters.
- No conversion to number: look for code that does
int(cnpj),parseInt,Convert.ToInt64or compares CNPJs as numbers. All of them break with a letter. - Uppercase: normalize to uppercase before storing and comparing, so that
12abc34501de35and12ABC34501DE35are the same record. Even if your database compares case-insensitively, comparisons inside your code and your integrations do not. - Input fields: replace numeric-only masks and keyboards (such as
inputmode="numeric") with fields that accept letters.
Look up an alphanumeric CNPJ via API
The lookup is the same as for any CNPJ: GET /{cnpj}, with the 14 characters and no mask. The API accepts uppercase and lowercase and validates the check digits of an alphanumeric CNPJ: with a wrong digit, the response is 422 (CNPJ inválido, invalid CNPJ). For numeric-only CNPJs, nothing changes. The same applies to the batch lookup, where a CNPJ with a wrong digit comes back as an invalido item.
To try it without an account, use the public test key with the fictitious sample 12ABC34501DE35 (see Authentication). The test key only responds to the sample CNPJs:
curl "https://api.cnpjapi.com.br/12ABC34501DE35" \
-H "Authorization: Bearer cnpj_teste_exemplos"
{
"CNPJ": "12.ABC.345/01DE-35",
"RazaoSocial": "EMPRESA EXEMPLO COM REGISTROS LTDA",
"MatrizFilial": { "Codigo": "1", "Descricao": "Matriz" }
}
With your own key, the lookup works for the real alphanumeric CNPJs in the Receita database, such as 00000000E08G12. The CNPJ field in the response always comes with the mask.
State Registration (IE)
The SEFAZ registry lookup does not accept alphanumeric CNPJs yet. So, for these CNPJs:
- the IE lookup (
GET /consulta/ie/{cnpj}) returns422; - in the CNPJ lookup with
?incluir=ie, the response returns the CNPJ data withInscricoesEstaduais: nullandInscricoesEstaduaisErro: "cnpj_alfanumerico", and you are not charged for the IE. This holds even without IE in your plan or with your IE quota used up: it counts as a regular CNPJ lookup.
Frequently asked questions
What is the alphanumeric CNPJ? It is a CNPJ with letters and numbers in the first 12 positions, which Receita Federal started issuing on July 31, 2026. It still has 14 characters and both check digits are always numbers.
Will existing CNPJs change? No. Existing CNPJs remain valid, with no replacement. The new format only applies to new registrations, issued gradually during 2026.
How do I calculate the check digits of an alphanumeric CNPJ? With the same modulo 11 as always, giving each character the value of its ASCII code minus 48: numbers are worth themselves and letters go from 17 (A) to 42 (Z).
Does the CNPJAPI API accept alphanumeric CNPJs? Yes. The GET /{cnpj} lookup accepts the new format, in uppercase or lowercase, and validates the check digits.
Can I look up the State Registration (IE) of an alphanumeric CNPJ? Not yet, because the SEFAZ registry lookup does not accept the new format. In the CNPJ lookup with IE, the response returns cnpj_alfanumerico in the IE error field, without charging for the IE.
Next steps
- Look up a CNPJ: every field in the response.
- Test key: the sample CNPJs, including the alphanumeric one.
- Batch CNPJ lookup: to revalidate your whole customer database.