Publicidad

Guías para desarrolladores

Regex para tarjetas de crédito: patrones de validación por marca

Las expresiones regulares son la forma más rápida de reconocer la marca de una tarjeta y descartar entradas mal formadas antes de enviar una solicitud de pago. Esta guía te da un patrón probado por marca, un patrón combinado, código funcional en tres lenguajes y los errores que hacen que un formulario de pago rechace tarjetas válidas.

Por el equipo de FakecreditcardPublicado 9 min de lectura

Publicidad

Puntos clave

  • Quita primero espacios y guiones y luego valida solo dígitos. Nunca obligues al cliente a escribir el número sin espacios.
  • Una regex comprueba el prefijo y la longitud, no si el número es real. Añade la verificación de Luhn para los errores de tecleo y deja que el emisor decida el resto.
  • El rango 2221–2720 de Mastercard, los números Visa y UnionPay de 19 dígitos y las longitudes variables de Maestro son donde fallan casi todos los patrones escritos a mano.
  • Usa la regex de marca para la experiencia de usuario (logo, máscara, longitud del CVC) y mantén permisiva la validación final: un prefijo desconocido también debe llegar a la pasarela.
En esta página
  1. Normaliza la entrada antes de comparar
  2. Regex de tarjetas de crédito para cada marca
  3. Cómo se construye cada patrón
  4. Por qué la regex es solo el primer paso
  5. Código listo para usar
  6. Errores que rechazan a clientes reales
  7. Prueba tus patrones con números generados
  8. Preguntas frecuentes

Casi todos los formularios de pago necesitan responder a dos preguntas mientras el cliente todavía escribe: ¿de qué marca es esta tarjeta? y ¿puede esto ser un número de tarjeta? Las expresiones regulares responden a ambas en microsegundos y sin ninguna llamada de red. Son las que muestran el logo de la marca tras los primeros dígitos, las que agrupan el número como 4-4-4-4 o 4-6-5 y las que hacen que el campo del código de seguridad pase de tres a cuatro dígitos.

Los patrones de esta guía están probados con decenas de miles de números creados por nuestro generador de tarjetas de prueba para cada marca y longitud, entre sí (ningún patrón acepta números de otra marca) y con una lista de casos límite negativos. Se comportan igual en JavaScript, Python y PHP.

Normaliza la entrada antes de comparar

La gente escribe el número tal como lo ve impreso: en grupos, con espacios, a veces con guiones y, de vez en cuando, con un espacio sobrante al final por culpa del autocompletado o de copiar y pegar. Tu regex no debería ver nada de eso. Elimina primero los separadores y compara después una cadena que contenga solo dígitos.

Quita los separadores, no todo

Elimina espacios y guiones con [\s-] y exige después que el resultado sean solo dígitos. Es tentador borrar cualquier carácter que no sea un dígito con \D, pero eso convierte en silencio 4111-1111-abcd-1111 en un número de aspecto válido. Si solo quitas los separadores esperados, la basura sigue fallando y puedes mostrar un mensaje útil.

Ayuda al navegador a ayudarte

Usa <input type="text" inputmode="numeric" autocomplete="cc-number">. Un campo type="number" elimina los ceros iniciales, añade flechas para subir y bajar y puede redondear valores largos, así que no sirve para un identificador de 16 a 19 dígitos. El token autocomplete permite que navegadores y gestores de contraseñas rellenen bien el número, lo que también reduce los errores de tecleo.

Regex de tarjetas de crédito para cada marca

Cada patrón comprueba dos cosas: el prefijo de identificación del emisor que usa la red y las longitudes que emite esa marca. Todos esperan solo dígitos, así que normaliza antes.

MarcaEmpieza porLongitudPatrón
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}$

Si solo necesitas un sí o un no para «parece una tarjeta admitida», los patrones se pueden unir en una sola expresión. Es larga, pero cada rama está anclada, así que sigue siendo rápida:

Patrón combinado (cualquier marca admitida)
^(?: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})$

Un patrón combinado no indica qué marca coincidió, por eso la mayoría de los formularios recorre los patrones individuales y se detiene en el primero que encaja. Los rangos de la tabla no se solapan, así que el orden no cambia el resultado.

Publicidad

Cómo se construye cada patrón

Entender la construcción facilita actualizar un patrón cuando una red añade un rango, algo que ocurre más a menudo de lo que suponen la mayoría de los proyectos.

Visa: un prefijo, tres longitudes

Los números Visa empiezan por 4. El patrón toma el 4, doce dígitos más (13 en total) y hasta dos grupos opcionales de tres dígitos, lo que da exactamente 13, 16 o 19 dígitos. Escribir [0-9]{12,18} aceptaría también 14, 15, 17 y 18 dígitos, longitudes que Visa no emite.

Mastercard: dos rangos en una expresión

El rango clásico es 51–55. El rango de la serie 2, de 2221 a 2720, no cabe en una sola clase de caracteres, así que se divide en bloques: 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) y 2720. Todos los números Mastercard, de crédito y de débito, tienen 16 dígitos.

American Express, Diners Club y JCB: la serie 3

Tres marcas comparten el prefijo 3. Amex es 34 o 37 con exactamente 15 dígitos. JCB es el rango de cuatro dígitos 3528–3589, escrito como 35(?:2[89]|[3-8][0-9]). Diners Club International usa 300–305, 36, 38 y 39; sus tarjetas clásicas tienen 14 dígitos, pero las más recientes pueden llegar a 19, así que el patrón admite de 14 a 19.

Discover, UnionPay y Maestro: la serie 6 y compañía

Discover usa 6011, 644–649 y 65. UnionPay usa 62, y Maestro una lista de prefijos de cuatro dígitos en los rangos 50, 58 y 6. Estas marcas emiten longitudes variables: Discover y UnionPay de 16 a 19 dígitos, y Maestro de 12 a 19, por eso sus cuantificadores son rangos.

Por qué la regex es solo el primer paso

Una regex demuestra que la entrada tiene la forma de un número de tarjeta de una marca concreta. Es útil, pero está muy lejos de demostrar que la tarjeta sea real. Trata la validación como capas, cada una de las cuales detecta un tipo de problema distinto.

CapaDetectaDónde
Normalización + regexLongitud o prefijo erróneos, letras, marca no admitidaNavegador y servidor
Suma de LuhnUn dígito mal tecleado y casi todos los dígitos vecinos intercambiadosNavegador y servidor
Fecha de vencimientoFechas pasadas, meses imposibles, años futuros poco realistasNavegador y servidor
Longitud del código de seguridad3 dígitos en casi todas las marcas, 4 en American ExpressNavegador
AutorizaciónSi la cuenta existe, está activa, tiene fondos y coincide con el CVV y la direcciónBanco emisor, a través de tu pasarela

Las cuatro primeras capas son de experiencia de usuario: permiten señalar un error de tecleo al instante en lugar de esperar la respuesta del banco. Solo la última tiene que ver con la verdad. Un número de un generador de tarjetas supera todas las comprobaciones de formato y aun así falla en la autorización, porque no hay ninguna cuenta detrás.

Código listo para usar

Las tres implementaciones siguientes se generan a partir de la misma tabla que los patrones, así que siempre están sincronizadas. Cada una elimina los separadores, rechaza todo lo que no tenga entre 12 y 19 dígitos, detecta la marca y después aplica la suma de Luhn. Devuelven la marca junto con un código de error legible por máquina que puedes asociar a tus propios mensajes.

JavaScript (navegador o 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}$/
};

// suma de verificación de Luhn sobre una cadena solo 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, '');  // primero quita espacios y guiones
  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:  # uno de cada dos dígitos desde la derecha
            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)  # primero quita espacios y guiones
    for brand, pattern in CARD_PATTERNS.items():
        if pattern.fullmatch(digits):  # fullmatch: no se cuela un salto de línea final
            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);  // primero quita espacios y guiones
    foreach (CARD_PATTERNS as $brand => $pattern) {
        if (preg_match($pattern, $digits) === 1) return $brand;  // el modificador D impide que $ coincida antes de un salto de línea 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]

Los patrones de PHP llevan el modificador D y el código de Python usa re.fullmatch. Sin ellos, $ en PHP y en Python también coincide justo antes de un salto de línea final, de modo que "4111111111111111\n" pasaría. En JavaScript, $ solo coincide al final real de la cadena salvo que añadas el flag m. Quitar antes los espacios en blanco cierra el hueco en los tres lenguajes, pero es mejor no depender de ello.

Errores que rechazan a clientes reales

Una regex demasiado estricta cuesta ventas: el cliente tiene una tarjeta válida, tu formulario dice que no lo es y se marcha. Estos son los errores que más aparecen en el código de pago real.

Olvidar la serie 2 de Mastercard

Los patrones escritos antes de 2016 suelen usar ^5[1-5][0-9]{14}$ para Mastercard. Las tarjetas del rango 2221–2720 llevan años emitiéndose y ese patrón antiguo las rechaza todas. Busca 5[1-5] en tu código: frontend, backend y cualquier script de analítica o de fraude.

Fijar la longitud en 16 dígitos

Visa emite números de 13, 16 y 19 dígitos, UnionPay y Discover llegan a 19 y Maestro va de 12 a 19. Un maxlength="16" en el campo o un cuantificador {16} bloquea a esos clientes antes incluso de que se ejecute tu validación. Permite 19 dígitos (y deja sitio para los espacios en el campo visible).

Rangos solapados y tarjetas de marca compartida

Los rangos de tarjetas no son perfectamente ordenados. Los números UnionPay del rango 622126–622925 también se aceptan en la red Discover, y los esquemas locales de muchos países emiten tarjetas dentro o al lado de los rangos de las grandes marcas. Los patrones de esta guía mantienen el 62 como UnionPay a propósito; si enrutas por marca, decide de forma explícita qué marca gana en un rango solapado y documéntalo junto a la regex.

Rechazar marcas que no reconoces

Esquemas regionales como RuPay, Elo, Mir, Verve o Troy no están en la tabla, pero tu pasarela puede aceptarlos. Usa las regex de marca para la presentación (logo, máscara, longitud del CVC), pero no bloquees el envío solo porque ninguna marca coincida. Si el número supera las comprobaciones de longitud y de Luhn, deja que decida el proveedor de pagos: tiene las tablas de BIN oficiales y actualizadas. Nuestro BIN checker muestra a quién pertenece un prefijo cuando necesitas investigarlo.

Validar en cada pulsación

Detecta la marca mientras el usuario escribe, pero muestra los mensajes de error solo cuando el campo pierde el foco o el número alcanza una longitud completa. Un formulario que muestra «número de tarjeta no válido» tras el primer dígito tiene razón técnicamente y resulta hostil en la práctica.

Prueba tus patrones con números generados

Los fallos de una regex se esconden en los bordes de los rangos, así que prueba los dos lados de cada límite. Un buen conjunto de pruebas tiene tres grupos:

  • Positivos para cada marca y cada longitud. Genera números por marca con nuestro generador o con las páginas de cada marca, como el generador de UnionPay, y escribe un patrón de BIN como 2720xxxxxxxxxxxx para forzar un prefijo concreto. Añade los números de prueba documentados por tu pasarela, que incluyen ejemplos de Diners de 14 dígitos y de Mastercard serie 2.
  • Negativos en los límites. Prefijos vecinos que no deben coincidir: 2220 y 2721 (Mastercard), 3527 y 3590 (JCB), 306 (Diners), 6012 y 643 (Discover), y longitudes un dígito más cortas o más largas de lo permitido.
  • Entradas basura. Letras, una cadena vacía, un salto de línea final y números con separadores que deben eliminarse antes de comparar.

Pasa los mismos casos por cada implementación que publiques (frontend, backend y cualquier app móvil), porque un patrón copiado de un lenguaje a otro puede cambiar de significado. El escapado, el comportamiento de $ y la diferencia entre match, search y fullmatch son los sospechosos habituales. Después, pega algunos números en el validador de tarjetas para confirmar a simple vista la marca y la suma de control.

Preguntas frecuentes

¿Cuál es la regex de una tarjeta Visa?

^4[0-9]{12}(?:[0-9]{3}){0,2}$ acepta números Visa de 13, 16 o 19 dígitos que empiezan por 4. Elimina espacios y guiones antes de comparar.

¿Cuál es la regex de Mastercard con la serie 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}$. Cubre el rango clásico 51–55 y el rango 2221–2720 (BIN 222100–272099) que Mastercard empezó a emitir a finales de 2016.

¿Puede una regex validar por completo un número de tarjeta?

No. Una regex solo comprueba el prefijo y la longitud. Usa la suma de Luhn para detectar errores de tecleo y recuerda que solo una autorización del banco emisor demuestra que la cuenta existe.

¿Es mejor una regex combinada o una por marca?

Usa un patrón por marca cuando necesites saber la marca (para mostrar el logo o fijar la longitud del CVC). Un patrón combinado sirve para comprobar rápidamente si algo «parece» un número de tarjeta, pero no dice qué marca coincidió.

¿Por qué mi regex acepta un número con un salto de línea al final?

En Python y PHP, $ también coincide justo antes de un salto de línea final. Quita primero los espacios en blanco, usa re.fullmatch en Python y añade el modificador D en PHP. En JavaScript, $ no tiene ese comportamiento salvo que uses el flag m.

¿Cómo pruebo mi regex de tarjetas?

Pásale números de un generador de tarjetas de prueba para cada marca y longitud, las tarjetas de prueba documentadas por tu pasarela y casos negativos: longitudes erróneas, prefijos vecinos (2220, 2721, 3527, 3590) y letras.

Fuentes

  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)

Herramientas de esta guía

Guías relacionadas

Escrito para desarrolladores, equipos de QA y comercios. Los números generados son datos de prueba: no son tarjetas reales y no sirven para pagar.

Publicidad