Skip to main content

Credit Card PAN Standards

From the CTO — Required reading for all product and engineering teams. This is not a suggestion. The rules in this document are enforced by PCI DSS. Non-compliance is a contractual, legal, and financial liability. If your code touches a card number in any form, you must follow this guide.


Table of Contents

  1. Why This Matters
  2. The Standard: ISO/IEC 7813 PAN Format
  3. The Golden Rule: Do Not Store the PAN
  4. Validation and Masking
  5. Database Best Practices
  6. Quick Reference

Why This Matters

A card number is not just a number. It is the key to someone's bank account.

The rules in this document exist because of PCI DSS — the Payment Card Industry Data Security Standard — which is a contractual requirement from Visa, Mastercard, and all major card networks. Violating it results in fines, loss of payment processing rights, and mandatory forensic audits that cost more than the fines.

What Goes Wrong Without a Standard

A data breach exposes raw card numbers. If a database column stores a full, unencrypted PAN and that database is breached, every card in it must be cancelled. The card networks fine the company per compromised card. These fines have ended companies. The breach is not hypothetical — it is the expected outcome of storing raw PANs without proper controls.

Logging captures card numbers in plaintext. A PAN passed as a query parameter, printed in an error message, or written to an application log is a PCI DSS violation even if the database is secure. Log pipelines are rarely covered by the same access controls as the payments database. One engineer with log access can read every card number.

Validation gaps allow invalid data into payment flows. A PAN that fails the Luhn check will be rejected by the card network's authorization API — but only after a network call is made. Validating before sending saves latency and prevents charging errors caused by typos.

Regex-only validation accepts structurally invalid cards. A regex like \d{16} accepts 1234567890123456, which is 16 digits but fails the Luhn check and will never authorize. Structural validation (length, prefix) combined with Luhn validation is the minimum required before any payment attempt.

Architectural warning: PCI DSS scope expands to every system that can read, transmit, or store a PAN. The cheapest compliance strategy is to keep the PAN out of your systems entirely. Use a payment processor's hosted fields or tokenization API. Your card_pan field should store a token, not a number.


The Standard: ISO/IEC 7813 PAN Format

ISO/IEC 7813 defines the structure of a Primary Account Number (PAN) as used on payment cards.

Format Rules

[IIN/BIN (6 digits)][Account number (variable)][Check digit (1 digit)]
ComponentDescriptionDetails
IIN / BINIssuer Identification Number (first 6 digits)Identifies the card network and issuing bank
Account numberVariable-length middle segmentAssigned by the issuer
Check digitLast digitComputed using the Luhn algorithm
Total length13–19 digitsMost common cards are 15 or 16 digits

Common Card Prefixes and Lengths

Card networkIIN prefix(es)Length
Visa413 or 16 digits
Mastercard5155, 2221272016 digits
American Express34, 3715 digits
Discover6011, 622126622925, 644649, 6516 digits
UnionPay6216–19 digits

What "Clean String" Means

We validate PANs as a plain digit string with no spaces, hyphens, or formatting characters.

User InputCanonical Form for Validation
4111 1111 1111 11114111111111111111
4111-1111-1111-11114111111111111111
41111111111111114111111111111111
411111111111111x❌ Invalid — non-digit character
41111111111111❌ Invalid — fails Luhn check

Rule: Strip spaces and hyphens before validation. Reject any input containing non-digit characters. Validate length (13–19) and then validate the Luhn check digit. Never store the result of this validation as the canonical value — see the next section.

The Luhn Algorithm

Every valid PAN passes the Luhn check. This is a required validation step before any payment attempt.

The algorithm:

  1. Starting from the rightmost digit (the check digit), double every second digit moving left.
  2. If doubling produces a number greater than 9, subtract 9.
  3. Sum all digits.
  4. If the total is divisible by 10, the number is valid.
PAN: 4111 1111 1111 1111
Step 1 (double every 2nd from right): 8 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1
Step 2 (subtract 9 where > 9): 8 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1
Step 3 (sum): = 30
Step 4 (30 % 10 == 0): ✅ Valid

The Golden Rule: Do Not Store the PAN

Do not store the full PAN in your database, your logs, your analytics events, or your cache.

This is the most important rule in this document. If you follow only one thing, follow this.

Use Tokenization

A payment processor (Stripe, Adyen, Braintree) provides a tokenization API. The user enters their card number in a form hosted by the processor — not your page. The processor returns a token: a non-sensitive string like tok_1234ABCD that represents the card. You store the token, not the PAN.

What you receive from the processorWhat you store in card_pan
Token: tok_1234ABCDtok_1234ABCD
Last 4 digits: 42424242 (for display only, in a separate column)
Card brand: visavisa (in a separate column)

The full PAN never reaches your server. Your PCI DSS scope is minimal. This is the correct architecture.

What You Are Allowed to Store

If you are operating under a PCI DSS certified environment (this requires formal certification — not a self-assessment), the rules are:

Data elementStorage allowedMust be encryptedMay be stored after authorization
Full PANYes, with controlsYesYes
Cardholder nameYesRecommendedYes
Expiry dateYesRecommendedYes
Service codeYesRecommendedYes
Full magnetic stripe dataNeverNever
CVV / CVC / CAV2NeverNever
PIN / PIN blockNeverNever

Rule: The CVV, CVC, and magnetic stripe data must never be stored under any circumstances, even temporarily. This is an absolute PCI DSS prohibition. No exception exists.


Validation and Masking

Validation

Validate a PAN in two steps: structural check, then Luhn check.

TypeScript:

function isValidPan(raw: string): boolean {
const digits = raw.replace(/[\s-]/g, '');
if (!/^\d{13,19}$/.test(digits)) return false;
// Luhn check
let sum = 0;
let double = false;
for (let i = digits.length - 1; i >= 0; i--) {
let d = parseInt(digits[i], 10);
if (double) {
d *= 2;
if (d > 9) d -= 9;
}
sum += d;
double = !double;
}
return sum % 10 === 0;
}

Go:

func isValidPAN(raw string) bool {
digits := strings.Map(func(r rune) rune {
if r >= '0' && r <= '9' {
return r
}
if r == ' ' || r == '-' {
return -1 // drop
}
return 0 // invalid character — signals rejection below
}, raw)

if len(digits) < 13 || len(digits) > 19 {
return false
}
for _, c := range digits {
if c == 0 {
return false // non-digit, non-separator found
}
}
// Luhn check
sum, double := 0, false
for i := len(digits) - 1; i >= 0; i-- {
d := int(digits[i] - '0')
if double {
d *= 2
if d > 9 {
d -= 9
}
}
sum += d
double = !double
}
return sum%10 == 0
}

Do not use a third-party package for Luhn validation. The algorithm is 15 lines and has no edge cases that a library solves better. Adding a dependency for this is unnecessary complexity.

Masking

When displaying a card number to a user — in a UI, an email, or a log — always mask it. Show only the last 4 digits. Never show more.

Correct: **** **** **** 4242
Incorrect: 4111 1111 1111 1111
Incorrect: 411111111111 1111 (partial mask)

TypeScript:

function maskPan(pan: string): string {
const digits = pan.replace(/\D/g, '');
return `**** **** **** ${digits.slice(-4)}`;
}

Go:

func maskPAN(pan string) string {
var digits strings.Builder
for _, c := range pan {
if c >= '0' && c <= '9' {
digits.WriteRune(c)
}
}
d := digits.String()
return fmt.Sprintf("**** **** **** %s", d[len(d)-4:])
}

Rule: Masking is for display only. The masked string must never be stored as the canonical value. Store the token, not the mask.

Logging Rules

Apply these rules in all logging middleware and error handlers before this standard can be violated by accident:

  • Scrub any string that matches \b\d{13,19}\b in log output.
  • Never log request bodies from payment endpoints verbatim.
  • Never log full card numbers in error messages — log the token and the last 4 digits only.

Database Best Practices

If You Are Storing Tokens (Correct Architecture)

The card_pan column stores the processor token, not a card number.

DecisionRecommendation
Column namecard_pan for the token (rename if clarity is needed: card_token)
TypeVARCHAR(100) — processor tokens vary in length
SensitiveNo — the token has no value outside the processor
Additional columnscard_last_four CHAR(4), card_brand VARCHAR(20) for display
-- Correct schema: tokens, not PANs
ALTER TABLE payment_methods
ADD COLUMN card_pan VARCHAR(100) NOT NULL, -- processor token
ADD COLUMN card_last_four CHAR(4) NOT NULL, -- display only
ADD COLUMN card_brand VARCHAR(20) NOT NULL; -- visa, mastercard, etc.

If You Are Storing Encrypted PANs (PCI DSS Certified Only)

This path requires formal PCI DSS certification. Do not proceed without it.

DecisionRecommendation
TypeTEXT or BYTEA — encrypted value is longer than the raw PAN
EncryptionAES-256-GCM with a key stored in a dedicated KMS (not in the DB)
Key rotationMandatory — see your KMS documentation
Access controlColumn-level or row-level security; minimize who can read it
Audit logEvery read of a PAN must be logged with user identity and timestamp

Do not encrypt the PAN with a key stored in the same database or the same environment variable file as the application. The encryption is only as strong as the key separation.


Quick Reference

✅ Store: tok_1234ABCD (processor token)
✅ Store separately: last four = 4242, brand = visa
❌ Never store: 4111111111111111 (full PAN)
❌ Never store: CVV, CVC, magnetic stripe data — under any circumstances
❌ Never log: any string matching \d{13,19}
TaskHow
Accept card inputUse processor hosted fields — PAN never reaches your server
Validate formatStrip spaces/hyphens → check length (13–19) → Luhn check
Store card referenceStore the processor token in card_pan
Display to userMask: **** **** **** 4242
Identify card typeStore card_brand and card_last_four as separate columns
Log a card errorLog token + last 4 only — never the full PAN
Check Luhn validity15-line function — no library needed
WhatAllowed to storeMust encrypt
Processor token✅ YesNo
Last 4 digits✅ YesNo
Card brand✅ YesNo
Full PAN⚠️ Only with PCI DSS certificationYes
CVV / CVC❌ Never
Magnetic stripe❌ Never

Questions or edge cases not covered here? Open a discussion in the #engineering-standards channel.