Publicidade

Guias para desenvolvedores

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.

Pela equipe FakecreditcardPublicado 9 min de leitura

Publicidade

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
  1. Normalize a entrada antes de comparar
  2. Regex de cartão de crédito para cada bandeira
  3. Como cada padrão é construído
  4. Por que a regex é só o primeiro passo
  5. Código pronto para usar
  6. Erros que recusam clientes reais
  7. Teste seus padrões com números gerados
  8. Perguntas frequentes

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.

BandeiraComeça comTamanhoPadrão
American Express34, 3715^3[47][0-9]{13}$
Diners Club300–305, 36, 38–3914–19^3(?:0[0-5]|[689][0-9])[0-9]{11,16}$
JCB3528–358916–19^35(?:2[89]|[3-8][0-9])[0-9]{12,15}$
Discover6011, 644–649, 6516–19^(?:6011|64[4-9][0-9]|65[0-9]{2})[0-9]{12,15}$
UnionPay6216–19^62[0-9]{14,17}$
Maestro5018, 5020, 5038, 5893, 6304, 6759, 6761–676312–19^(?:5018|5020|5038|5893|6304|6759|676[1-3])[0-9]{8,15}$
Mastercard51–55, 2221–272016^(?: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}$
Visa413, 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:

Padrão combinado (qualquer bandeira aceita)
^(?: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.

Publicidade

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.

CamadaPegaOnde
Normalização + regexTamanho ou prefixo errado, letras, bandeira não aceitaNavegador e servidor
Checksum de LuhnUm dígito digitado errado e quase todas as trocas de dígitos vizinhosNavegador e servidor
Data de validadeDatas passadas, meses impossíveis, anos futuros irreaisNavegador e servidor
Tamanho do código de segurança3 dígitos na maioria das bandeiras, 4 na American ExpressNavegador
AutorizaçãoSe a conta existe, está ativa, tem saldo e confere com o CVV e o endereçoBanco 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)

JavaScript
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+

Python
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
<?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 2720xxxxxxxxxxxx para 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: 2220 e 2721 (Mastercard), 3527 e 3590 (JCB), 306 (Diners), 6012 e 643 (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

  1. Adyen — Mastercard launches the 2-series BIN range 222100–272099 (Portuguese)
  2. MDN — Input boundary assertion: $
  3. Python documentation — re.fullmatch
  4. PHP manual — Pattern modifiers (D, PCRE_DOLLAR_ENDONLY)

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.

Publicidade