Why You Need a Credit Card Regex
Regex is your first line of defense against typos. Before you make a network call to Stripe, Adyen or Braintree, a quick client-side regex can tell the user "that looks wrong" without burning an API request. It also lets you show the correct card-brand logo in your form as the user types — a small UX touch that raises checkout conversion.
What regex does not do: it does not confirm the card is real, has funds, or belongs to the customer. Never treat regex as security. It is a UX helper.
Per-Brand Regex Patterns
The regexes below match the digits-only form of each card number (after stripping spaces and dashes):
- Visa:
/^4[0-9]{12}(?:[0-9]{3})?$/— 13 or 16 digits starting with 4. - Mastercard:
/^(5[1-5][0-9]{14}|2(2[2-9][1-9]|[3-6][0-9]{2}|7[01][0-9]|720)[0-9]{12})$/— 16 digits starting with 51–55 (legacy) or 2221–2720 (2017+ BIN expansion). - Amex:
/^3[47][0-9]{13}$/— 15 digits starting with 34 or 37. - Discover:
/^6(?:011|5[0-9]{2})[0-9]{12}$/— 16 digits starting with 6011 or 65. - Diners Club:
/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/— 14 digits. - JCB:
/^(?:2131|1800|35\d{3})\d{11}$/— 15 or 16 digits.
Language-Specific Usage
JavaScript
const CARDS = {
visa: /^4[0-9]{12}(?:[0-9]{3})?$/,
mastercard: /^(5[1-5][0-9]{14}|2(2[2-9][1-9]|[3-6][0-9]{2}|7[01][0-9]|720)[0-9]{12})$/,
amex: /^3[47][0-9]{13}$/,
discover: /^6(?:011|5[0-9]{2})[0-9]{12}$/,
};
function detectBrand(raw) {
const digits = raw.replace(/[\s-]/g, '');
for (const [brand, pattern] of Object.entries(CARDS)) {
if (pattern.test(digits)) return brand;
}
return null;
}
detectBrand('4242 4242 4242 4242'); // 'visa'
detectBrand('378282246310005'); // 'amex'
detectBrand('1234567890'); // null
Python
import re
CARDS = {
'visa': r'^4[0-9]{12}(?:[0-9]{3})?$',
'mastercard': r'^(5[1-5][0-9]{14}|2(2[2-9][1-9]|[3-6][0-9]{2}|7[01][0-9]|720)[0-9]{12})$',
'amex': r'^3[47][0-9]{13}$',
'discover': r'^6(?:011|5[0-9]{2})[0-9]{12}$',
}
def detect_brand(raw):
digits = re.sub(r'[\s-]', '', raw)
for brand, pattern in CARDS.items():
if re.match(pattern, digits):
return brand
return None
print(detect_brand('4242 4242 4242 4242')) # visa
print(detect_brand('378282246310005')) # amex
PHP
<?php
$cards = [
'visa' => '/^4[0-9]{12}(?:[0-9]{3})?$/',
'mastercard' => '/^(5[1-5][0-9]{14}|2(2[2-9][1-9]|[3-6][0-9]{2}|7[01][0-9]|720)[0-9]{12})$/',
'amex' => '/^3[47][0-9]{13}$/',
'discover' => '/^6(?:011|5[0-9]{2})[0-9]{12}$/',
];
function detectBrand($raw, $cards) {
$digits = preg_replace('/[\s-]/', '', $raw);
foreach ($cards as $brand => $pattern) {
if (preg_match($pattern, $digits)) return $brand;
}
return null;
}
echo detectBrand('4242 4242 4242 4242', $cards); // visa
Regex Is Not Enough — Add Luhn
The regex /^4[0-9]{15}$/ matches 4000000000000000 just as eagerly as 4242424242424242, but only the second one passes the Luhn (mod-10) checksum every real card obeys. Combine both checks:
function luhn(num) {
let sum = 0, alt = false;
for (let i = num.length - 1; i >= 0; i--) {
let n = parseInt(num.charAt(i), 10);
if (alt) { n *= 2; if (n > 9) n -= 9; }
sum += n;
alt = !alt;
}
return sum % 10 === 0;
}
function isValidCard(raw) {
const digits = raw.replace(/[\s-]/g, '');
return /^[0-9]{13,19}$/.test(digits) && luhn(digits);
}
isValidCard('4242 4242 4242 4242'); // true
isValidCard('4000 0000 0000 0000'); // false (Luhn fails)
PCI-DSS: Regex Does Not Reduce Your Scope
The moment a full PAN (Primary Account Number) hits any server you control — even in a log file, even in a validation function that echoes back an error — you fall under PCI-DSS scope. That means annual audits, quarterly network scans, and strict data-retention rules.
The right pattern is to use a tokenisation provider's hosted fields (Stripe Elements, Adyen Web Components, Braintree Hosted Fields, Square Web Payments SDK). The card number never touches your JavaScript or your servers — it goes straight from the browser to the processor's iframe. You get back a token like tok_1LZm... which you can store safely.
Common Test Card Numbers
These are the test numbers Stripe, Adyen and PayPal publish for sandbox environments — all pass regex and Luhn:
- 4242 4242 4242 4242 — Visa (Stripe default)
- 5555 5555 5555 4444 — Mastercard
- 3782 822463 10005 — American Express
- 6011 1111 1111 1117 — Discover
- 3056 9309 0259 04 — Diners Club
- 3530 1113 3330 0000 — JCB
These numbers never post real charges — they are safe to hard-code in test suites and demo forms. For production always tokenise.
Explore Other Regex Patterns
- Regex for email validation — the daily-workhorse pattern.
- Regex for phone numbers — E.164, US formats and more.
- Regex for password validation — strength rules and quantifiers.
- Regex for IP addresses — IPv4 and IPv6.
- Regex for 24-hour time — HH:MM validation.
- Regex for domain names — bare-domain matching.
Frequently Asked Questions
Can I use regex to detect if a card is Visa or Mastercard?
Yes — see the per-brand patterns above. The first 1–4 digits (the IIN/BIN) uniquely identify the network.
What about Maestro and UnionPay?
Maestro: /^(5018|5020|5038|6304|6759|676[1-3])[0-9]{8,15}$/ (12 to 19 digits). UnionPay: /^(62|88)[0-9]{14,17}$/ (16 to 19 digits).
Is it OK to store credit card numbers if I hash them?
No. PCI-DSS still applies. Even hashed PANs are considered cardholder data. Use tokenisation.
Why does the Mastercard regex look so complicated?
In 2017 Mastercard added the 2221–2720 BIN range to relieve pressure on the 51–55 range. Regex has to cover both, so the alternation looks messy.