Regex de cartão de crédito: padrões de validação por bandeira
Expressões regulares são o jeito mais rápido de reconhecer a bandeira de um cartão e barrar entradas malformadas antes de enviar uma requisição de pagamento. Este guia traz um padrão testado por bandeira, um padrão combinado, código funcional em três linguagens e os erros que fazem um checkout recusar cartões válidos.
Pontos-chave
- Remova espaços e hifens primeiro e depois valide só dígitos. Nunca obrigue o cliente a digitar o número sem espaços.
- Uma regex confere o prefixo e o tamanho, não se o número é real. Some a verificação de Luhn para erros de digitação e deixe o emissor decidir o resto.
- A faixa 2221–2720 da Mastercard, os números Visa e UnionPay de 19 dígitos e os tamanhos variáveis do Maestro são onde quase todo padrão escrito à mão falha.
- Use a regex de bandeira para a experiência do usuário (logo, máscara, tamanho do CVC) e mantenha a validação final permissiva: um prefixo desconhecido também deve chegar ao gateway.
Nesta página
Quase todo formulário de pagamento precisa responder a duas perguntas enquanto o cliente ainda está digitando: de qual bandeira é este cartão? e isto pode ser um número de cartão? Expressões regulares respondem às duas em microssegundos, sem nenhuma chamada de rede. São elas que exibem o logo da bandeira depois dos primeiros dígitos, que agrupam o número como 4-4-4-4 ou 4-6-5 e que fazem o campo do código de segurança alternar entre três e quatro dígitos.
Os padrões deste guia foram testados com dezenas de milhares de números criados pelo nosso gerador de cartões de teste para cada bandeira e tamanho, entre si (nenhum padrão aceita números de outra bandeira) e com uma lista de casos-limite negativos. Eles se comportam da mesma forma em JavaScript, Python e PHP.
Normalize a entrada antes de comparar
As pessoas digitam o número do cartão do jeito que o veem impresso: em grupos, com espaços, às vezes com hifens e, de vez em quando, com um espaço sobrando no fim por causa do preenchimento automático ou de um copiar e colar. A sua regex não deveria ver nada disso. Remova primeiro os separadores e depois compare uma string que contenha só dígitos.
Remova os separadores, não tudo
Tire espaços e hifens com [\s-] e então exija que o resultado tenha só dígitos. É tentador apagar qualquer caractere que não seja dígito com \D, mas isso transforma silenciosamente 4111-1111-abcd-1111 em um número com cara de válido. Removendo apenas os separadores esperados, o lixo continua falhando e você pode mostrar uma mensagem útil.
Ajude o navegador a ajudar você
Use <input type="text" inputmode="numeric" autocomplete="cc-number">. Um campo type="number" descarta zeros à esquerda, adiciona setinhas de incremento e pode arredondar valores longos, então é o controle errado para um identificador de 16 a 19 dígitos. O token autocomplete permite que navegadores e gerenciadores de senhas preencham o número corretamente, o que também reduz erros de digitação.
Regex de cartão de crédito para cada bandeira
Cada padrão confere duas coisas: o prefixo de identificação do emissor usado pela bandeira e os tamanhos que ela emite. Todos esperam apenas dígitos, então normalize antes.
| Bandeira | Começa com | Tamanho | Padrão |
|---|---|---|---|
| American Express | 34, 37 | 15 | ^3[47][0-9]{13}$ |
| Diners Club | 300–305, 36, 38–39 | 14–19 | ^3(?:0[0-5]|[689][0-9])[0-9]{11,16}$ |
| JCB | 3528–3589 | 16–19 | ^35(?:2[89]|[3-8][0-9])[0-9]{12,15}$ |
| Discover | 6011, 644–649, 65 | 16–19 | ^(?:6011|64[4-9][0-9]|65[0-9]{2})[0-9]{12,15}$ |
| UnionPay | 62 | 16–19 | ^62[0-9]{14,17}$ |
| Maestro | 5018, 5020, 5038, 5893, 6304, 6759, 6761–6763 | 12–19 | ^(?:5018|5020|5038|5893|6304|6759|676[1-3])[0-9]{8,15}$ |
| Mastercard | 51–55, 2221–2720 | 16 | ^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$ |
| Visa | 4 | 13, 16, 19 | ^4[0-9]{12}(?:[0-9]{3}){0,2}$ |
Se você só precisa de um sim ou não para “parece um cartão aceito”, os padrões podem ser unidos em uma única expressão. Ela é longa, mas cada ramo é ancorado, então continua rápida:
^(?:3[47][0-9]{13}|3(?:0[0-5]|[689][0-9])[0-9]{11,16}|35(?:2[89]|[3-8][0-9])[0-9]{12,15}|(?:6011|64[4-9][0-9]|65[0-9]{2})[0-9]{12,15}|62[0-9]{14,17}|(?:5018|5020|5038|5893|6304|6759|676[1-3])[0-9]{8,15}|(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|4[0-9]{12}(?:[0-9]{3}){0,2})$Um padrão combinado não informa qual bandeira casou, por isso a maioria dos formulários percorre os padrões individuais e para no primeiro que bate. As faixas da tabela não se sobrepõem, então a ordem não muda o resultado.
Como cada padrão é construído
Entender a construção facilita atualizar um padrão quando uma bandeira acrescenta uma faixa, algo que acontece com mais frequência do que a maioria dos projetos imagina.
Visa: um prefixo, três tamanhos
Os números Visa começam com 4. O padrão pega o 4, mais doze dígitos (13 no total) e até dois grupos opcionais de três dígitos, o que dá exatamente 13, 16 ou 19 dígitos. Escrever [0-9]{12,18} aceitaria também 14, 15, 17 e 18 dígitos, tamanhos que a Visa não emite.
Mastercard: duas faixas em uma expressão
A faixa clássica é 51–55. A faixa da série 2, de 2221 a 2720, não cabe em uma única classe de caracteres, então é dividida em blocos: 222[1-9] (2221–2229), 22[3-9][0-9] (2230–2299), 2[3-6][0-9]{2} (2300–2699), 27[01][0-9] (2700–2719) e 2720. Todos os números Mastercard, de crédito e de débito, têm 16 dígitos.
American Express, Diners Club e JCB: a série 3
Três bandeiras dividem o prefixo 3. A Amex é 34 ou 37, com exatamente 15 dígitos. A JCB é a faixa de quatro dígitos 3528–3589, escrita como 35(?:2[89]|[3-8][0-9]). O Diners Club International usa 300–305, 36, 38 e 39; os cartões clássicos têm 14 dígitos, mas os mais novos podem chegar a 19, então o padrão aceita de 14 a 19.
Discover, UnionPay e Maestro: a série 6 e companhia
A Discover usa 6011, 644–649 e 65. A UnionPay usa 62, e o Maestro uma lista de prefixos de quatro dígitos nas faixas 50, 58 e 6. Essas bandeiras emitem tamanhos variáveis: Discover e UnionPay de 16 a 19 dígitos, e Maestro de 12 a 19, por isso os quantificadores delas são intervalos.
Por que a regex é só o primeiro passo
Uma regex prova que a entrada tem o formato de um número de cartão de determinada bandeira. Isso é útil, mas está muito longe de provar que o cartão é real. Pense na validação como camadas, cada uma pegando um tipo diferente de problema.
| Camada | Pega | Onde |
|---|---|---|
| Normalização + regex | Tamanho ou prefixo errado, letras, bandeira não aceita | Navegador e servidor |
| Checksum de Luhn | Um dígito digitado errado e quase todas as trocas de dígitos vizinhos | Navegador e servidor |
| Data de validade | Datas passadas, meses impossíveis, anos futuros irreais | Navegador e servidor |
| Tamanho do código de segurança | 3 dígitos na maioria das bandeiras, 4 na American Express | Navegador |
| Autorização | Se a conta existe, está ativa, tem saldo e confere com o CVV e o endereço | Banco emissor, via seu gateway |
As quatro primeiras camadas são de experiência do usuário: elas apontam um erro de digitação na hora, em vez de esperar a resposta do banco. Só a última tem a ver com a verdade. Um número de um gerador de cartões passa por todas as verificações de formato e mesmo assim falha na autorização, porque não existe conta por trás dele.
Código pronto para usar
As três implementações abaixo são geradas a partir da mesma tabela dos padrões, então ficam sempre sincronizadas. Cada uma remove os separadores, rejeita tudo o que não tiver entre 12 e 19 dígitos, detecta a bandeira e então aplica o checksum de Luhn. Elas devolvem a bandeira junto com um código de erro legível por máquina, que você pode associar às suas próprias mensagens.
JavaScript (navegador ou Node.js)
const CARD_PATTERNS = {
amex: /^3[47][0-9]{13}$/,
diners: /^3(?:0[0-5]|[689][0-9])[0-9]{11,16}$/,
jcb: /^35(?:2[89]|[3-8][0-9])[0-9]{12,15}$/,
discover: /^(?:6011|64[4-9][0-9]|65[0-9]{2})[0-9]{12,15}$/,
unionpay: /^62[0-9]{14,17}$/,
maestro: /^(?:5018|5020|5038|5893|6304|6759|676[1-3])[0-9]{8,15}$/,
mastercard: /^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$/,
visa: /^4[0-9]{12}(?:[0-9]{3}){0,2}$/
};
// checksum de Luhn sobre uma string só de dígitos
function luhnValid(digits) {
let sum = 0;
for (let i = digits.length - 1, dbl = false; i >= 0; i--, dbl = !dbl) {
let d = digits.charCodeAt(i) - 48;
if (dbl) { d *= 2; if (d > 9) d -= 9; }
sum += d;
}
return sum % 10 === 0;
}
function detectBrand(input) {
const digits = String(input).replace(/[\s-]/g, ''); // remova espaços e hifens primeiro
for (const [brand, re] of Object.entries(CARD_PATTERNS)) {
if (re.test(digits)) return brand;
}
return null;
}
function validateCardNumber(input) {
const digits = String(input).replace(/[\s-]/g, '');
if (!/^[0-9]{12,19}$/.test(digits)) return { valid: false, brand: null, error: 'format' };
const brand = detectBrand(digits);
if (!brand) return { valid: false, brand: null, error: 'unknown_brand' };
if (!luhnValid(digits)) return { valid: false, brand, error: 'checksum' };
return { valid: true, brand, error: null };
}
console.log(validateCardNumber('4111 1111 1111 1111')); // { valid: true, brand: 'visa', error: null }
console.log(validateCardNumber('4111 1111 1111 1112')); // { valid: false, brand: 'visa', error: 'checksum' }Python 3.10+
import re
CARD_PATTERNS = {
'amex': re.compile(r'^3[47][0-9]{13}$'),
'diners': re.compile(r'^3(?:0[0-5]|[689][0-9])[0-9]{11,16}$'),
'jcb': re.compile(r'^35(?:2[89]|[3-8][0-9])[0-9]{12,15}$'),
'discover': re.compile(r'^(?:6011|64[4-9][0-9]|65[0-9]{2})[0-9]{12,15}$'),
'unionpay': re.compile(r'^62[0-9]{14,17}$'),
'maestro': re.compile(r'^(?:5018|5020|5038|5893|6304|6759|676[1-3])[0-9]{8,15}$'),
'mastercard': re.compile(r'^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$'),
'visa': re.compile(r'^4[0-9]{12}(?:[0-9]{3}){0,2}$'),
}
SEPARATORS = re.compile(r'[\s-]')
def luhn_valid(digits: str) -> bool:
total = 0
for i, ch in enumerate(reversed(digits)):
d = int(ch)
if i % 2: # um a cada dois dígitos, a partir da direita
d = d * 2 - 9 if d > 4 else d * 2
total += d
return total % 10 == 0
def detect_brand(number: str) -> str | None:
digits = SEPARATORS.sub('', number) # remova espaços e hifens primeiro
for brand, pattern in CARD_PATTERNS.items():
if pattern.fullmatch(digits): # fullmatch: nenhuma quebra de linha final passa
return brand
return None
def validate_card_number(number: str) -> tuple[bool, str | None, str | None]:
digits = SEPARATORS.sub('', number)
if not re.fullmatch(r'[0-9]{12,19}', digits):
return False, None, 'format'
brand = detect_brand(digits)
if brand is None:
return False, None, 'unknown_brand'
if not luhn_valid(digits):
return False, brand, 'checksum'
return True, brand, None
print(validate_card_number('3782 822463 10005')) # (True, 'amex', None)PHP 8
<?php
const CARD_PATTERNS = [
'amex' => '/^3[47][0-9]{13}$/D',
'diners' => '/^3(?:0[0-5]|[689][0-9])[0-9]{11,16}$/D',
'jcb' => '/^35(?:2[89]|[3-8][0-9])[0-9]{12,15}$/D',
'discover' => '/^(?:6011|64[4-9][0-9]|65[0-9]{2})[0-9]{12,15}$/D',
'unionpay' => '/^62[0-9]{14,17}$/D',
'maestro' => '/^(?:5018|5020|5038|5893|6304|6759|676[1-3])[0-9]{8,15}$/D',
'mastercard' => '/^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$/D',
'visa' => '/^4[0-9]{12}(?:[0-9]{3}){0,2}$/D',
];
function luhn_valid(string $digits): bool
{
$sum = 0;
$double = false;
for ($i = strlen($digits) - 1; $i >= 0; $i--) {
$d = (int) $digits[$i];
if ($double) {
$d *= 2;
if ($d > 9) $d -= 9;
}
$sum += $d;
$double = !$double;
}
return $sum % 10 === 0;
}
function detect_brand(string $input): ?string
{
$digits = preg_replace('/[\s-]/', '', $input); // remova espaços e hifens primeiro
foreach (CARD_PATTERNS as $brand => $pattern) {
if (preg_match($pattern, $digits) === 1) return $brand; // o modificador D impede que $ case antes de uma quebra de linha final
}
return null;
}
function validate_card_number(string $input): array
{
$digits = preg_replace('/[\s-]/', '', $input);
if (preg_match('/^[0-9]{12,19}$/D', $digits) !== 1) return [false, null, 'format'];
$brand = detect_brand($digits);
if ($brand === null) return [false, null, 'unknown_brand'];
if (!luhn_valid($digits)) return [false, $brand, 'checksum'];
return [true, $brand, null];
}
var_dump(validate_card_number('5555-5555-5555-4444')); // [true, 'mastercard', null]Os padrões em PHP levam o modificador D e o código Python usa re.fullmatch. Sem eles, $ em PHP e em Python também casa logo antes de uma quebra de linha final, e "4111111111111111\n" passaria. Em JavaScript, $ só casa no fim real da string, a menos que você acrescente a flag m. Remover os espaços em branco antes fecha a brecha nas três linguagens, mas é melhor não depender disso.
Erros que recusam clientes reais
Uma regex rígida demais custa vendas: o cliente tem um cartão válido, o seu formulário diz que ele é inválido e o cliente vai embora. Estes são os erros que mais aparecem em código de checkout de verdade.
Esquecer a série 2 da Mastercard
Padrões escritos antes de 2016 costumam usar ^5[1-5][0-9]{14}$ para a Mastercard. Cartões da faixa 2221–2720 são emitidos há anos, e esse padrão antigo recusa todos eles. Procure 5[1-5] no seu código: front-end, back-end e qualquer script de analytics ou antifraude.
Fixar o tamanho em 16 dígitos
A Visa emite números de 13, 16 e 19 dígitos, UnionPay e Discover chegam a 19 e o Maestro vai de 12 a 19. Um maxlength="16" no campo ou um quantificador {16} bloqueia esses clientes antes mesmo de a sua validação rodar. Aceite 19 dígitos (e deixe espaço para os espaços no campo visível).
Faixas sobrepostas e cartões co-branded
As faixas de cartões não são perfeitamente organizadas. Números UnionPay na faixa 622126–622925 também são aceitos na rede Discover, e esquemas locais de muitos países emitem cartões dentro ou ao lado das faixas das grandes bandeiras — no Brasil, por exemplo, Elo e Hipercard. Os padrões deste guia mantêm o 62 como UnionPay de propósito; se você roteia por bandeira, decida explicitamente qual bandeira vence numa faixa sobreposta e documente isso junto da regex.
Recusar bandeiras que você não reconhece
Esquemas regionais como Elo, RuPay, Mir, Verve ou Troy não estão na tabela, mas o seu gateway pode aceitá-los. Use as regex de bandeira para a apresentação (logo, máscara, tamanho do CVC), mas não bloqueie o envio só porque nenhuma bandeira casou. Se o número passar nas verificações de tamanho e de Luhn, deixe o provedor de pagamentos decidir: ele tem as tabelas de BIN oficiais e atualizadas. O nosso BIN checker mostra a quem pertence um prefixo quando você precisa investigar.
Validar a cada tecla
Detecte a bandeira enquanto o usuário digita, mas mostre mensagens de erro só quando o campo perder o foco ou o número atingir um tamanho completo. Um formulário que exibe “número de cartão inválido” depois do primeiro dígito está tecnicamente certo e é hostil na prática.
Teste seus padrões com números gerados
Bugs de regex se escondem nas bordas das faixas, então teste os dois lados de cada limite. Um bom conjunto de testes tem três grupos:
- Positivos para cada bandeira e cada tamanho. Gere números por bandeira com o nosso gerador ou com as páginas de cada bandeira, como o gerador de UnionPay, e digite um padrão de BIN como
2720xxxxxxxxxxxxpara forçar um prefixo específico. Acrescente os números de teste documentados pelo seu gateway, que incluem exemplos de Diners com 14 dígitos e de Mastercard série 2. - Negativos nos limites. Prefixos vizinhos que não podem casar:
2220e2721(Mastercard),3527e3590(JCB),306(Diners),6012e643(Discover), e tamanhos um dígito menores ou maiores que o permitido. - Entradas lixo. Letras, uma string vazia, uma quebra de linha final e números com separadores que devem ser removidos antes da comparação.
Rode os mesmos casos em todas as implementações que você publica (front-end, back-end e qualquer app mobile), porque um padrão copiado de uma linguagem para outra pode mudar de significado. Escape de caracteres, o comportamento de $ e a diferença entre match, search e fullmatch são os suspeitos de sempre. Depois, cole alguns números no validador de cartões para confirmar a bandeira e o checksum a olho.
Perguntas frequentes
Qual é a regex de um cartão Visa?
^4[0-9]{12}(?:[0-9]{3}){0,2}$ aceita números Visa de 13, 16 ou 19 dígitos que começam com 4. Remova espaços e hifens antes de comparar.
Qual é a regex da Mastercard com a série 2?
^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$. Ela cobre a faixa clássica 51–55 e a faixa 2221–2720 (BINs 222100–272099), que a Mastercard começou a emitir no fim de 2016.
Uma regex consegue validar um número de cartão por completo?
Não. Uma regex só confere o prefixo e o tamanho. Use o checksum de Luhn para pegar erros de digitação e lembre que só uma autorização do banco emissor prova que a conta existe.
É melhor uma regex combinada ou uma por bandeira?
Use um padrão por bandeira quando precisar saber a bandeira (para mostrar o logo ou definir o tamanho do CVC). Um padrão combinado serve para checar rapidamente se algo “parece” um número de cartão, mas não diz qual bandeira casou.
Por que minha regex aceita um número com quebra de linha no fim?
Em Python e PHP, $ também casa logo antes de uma quebra de linha final. Remova os espaços em branco primeiro, use re.fullmatch em Python e acrescente o modificador D em PHP. Em JavaScript, $ não tem esse comportamento, a menos que você use a flag m.
Como testo minha regex de cartão?
Rode-a contra números de um gerador de cartões de teste para cada bandeira e tamanho, contra os cartões de teste documentados pelo seu gateway e contra casos negativos: tamanhos errados, prefixos vizinhos (2220, 2721, 3527, 3590) e letras.
Fontes
Ferramentas deste guia
Guias relacionados
Escrito para desenvolvedores, equipes de QA e lojistas. Os números gerados são dados de teste: não são cartões reais e não servem para pagar.