Match a credit card number
Confirm a card number field holds 13 to 19 digits, with optional spaces or dashes between groups, before you even think about Luhn.
Last updated
Matches
- 4111 1111 1111 1111
- 4111-1111-1111-1111
- 4111111111111111
Doesn’t match
- 1234
- 4111 1111 1111
- 4111-1111-1111-abcd
Why it’s written this way
The pattern opens with a single \d, then repeats (?:[ -]?\d) between 12 and 18 more times — so the total digit count lands anywhere from 13 to 19, matching the range real card networks issue (16 for Visa and Mastercard, 15 for Amex, up to 19 for some debit and UnionPay cards).
Each digit after the first can be preceded by an optional space or dash, so '4111 1111 1111 1111' and '4111-1111-1111-1111' both pass alongside the unspaced form — the separator is permitted, never required.
Edge cases to know
- →Matching the shape is not validating the number — a 16-digit string that happens to fit the length range still needs a Luhn checksum pass before you treat it as a genuine card number.
- →It doesn't identify the card network or check issuer-specific prefixes; telling Visa from Mastercard needs a separate lookup table.
- →Card numbers are sensitive under PCI DSS — validating the format client-side is fine, but think carefully before this value ever reaches your own logs, database, or error reports unmasked.
Part by part
- ^\d
- A required leading digit, so a match cannot begin on a space or a hyphen.
- (?:[ -]?\d){12,18}
- Twelve to eighteen further digits, each optionally preceded by a space or hyphen. This is what accepts the grouped form people actually type.
- $
- Anchored, because this validates a whole field rather than finding a number in text.
Common mistakes
^\d{16}$Sixteen digits describes one common length and rejects the rest: American Express is fifteen, Diners fourteen, and several networks issue nineteen. It also rejects any number entered with spaces.
^\d(?:[ -]?\d){12,18}$^[\d -]{13,19}$A flat class allows separators anywhere, including at the start and end and several in a row, so "----------------" validates. It also stops counting digits, which is the thing that mattered.
^\d(?:[ -]?\d){12,18}$ used as the only checkShape without a checksum accepts a mistyped digit, which is the single most common data-entry error. This pattern should always be paired with a Luhn check.
Questions
Does it verify the number is real?
No. It checks length and permitted separators only. Run the Luhn checksum afterwards, which catches most single-digit typos and transpositions, and remember that even a Luhn-valid number may not be issued to anyone.
Should I be handling card numbers at all?
Almost certainly not directly. Payment providers offer hosted fields precisely so the number never touches your server, which removes most of your PCI obligations. Use this to validate before handing off, not to store.
Why allow spaces and hyphens?
Because cards are printed in groups of four and people type what they see. Rejecting the separators produces a rejection the user cannot explain — strip them after matching instead of forbidding them.
Can I identify the card network from the number?
The leading digits indicate it — 4 for Visa, 51 to 55 and 22 to 27 for Mastercard, 34 and 37 for Amex. Use it to show the right logo, never to decide whether the number is acceptable.