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
- Why This Matters
- The Standard: ISO/IEC 7813 PAN Format
- The Golden Rule: Do Not Store the PAN
- Validation and Masking
- Database Best Practices
- 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_panfield 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)]
| Component | Description | Details |
|---|---|---|
| IIN / BIN | Issuer Identification Number (first 6 digits) | Identifies the card network and issuing bank |
| Account number | Variable-length middle segment | Assigned by the issuer |
| Check digit | Last digit | Computed using the Luhn algorithm |
| Total length | 13–19 digits | Most common cards are 15 or 16 digits |
Common Card Prefixes and Lengths
| Card network | IIN prefix(es) | Length |
|---|---|---|
| Visa | 4 | 13 or 16 digits |
| Mastercard | 51–55, 2221–2720 | 16 digits |
| American Express | 34, 37 | 15 digits |
| Discover | 6011, 622126–622925, 644–649, 65 | 16 digits |
| UnionPay | 62 | 16–19 digits |
What "Clean String" Means
We validate PANs as a plain digit string with no spaces, hyphens, or formatting characters.
| User Input | Canonical Form for Validation |
|---|---|
4111 1111 1111 1111 | 4111111111111111 |
4111-1111-1111-1111 | 4111111111111111 |
4111111111111111 | 4111111111111111 |
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:
- Starting from the rightmost digit (the check digit), double every second digit moving left.
- If doubling produces a number greater than 9, subtract 9.
- Sum all digits.
- 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 processor | What you store in card_pan |
|---|---|
Token: tok_1234ABCD | tok_1234ABCD |
Last 4 digits: 4242 | 4242 (for display only, in a separate column) |
Card brand: visa | visa (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 element | Storage allowed | Must be encrypted | May be stored after authorization |
|---|---|---|---|
| Full PAN | Yes, with controls | Yes | Yes |
| Cardholder name | Yes | Recommended | Yes |
| Expiry date | Yes | Recommended | Yes |
| Service code | Yes | Recommended | Yes |
| Full magnetic stripe data | Never | — | Never |
| CVV / CVC / CAV2 | Never | — | Never |
| PIN / PIN block | Never | — | Never |
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}\bin 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.
| Decision | Recommendation |
|---|---|
| Column name | card_pan for the token (rename if clarity is needed: card_token) |
| Type | VARCHAR(100) — processor tokens vary in length |
| Sensitive | No — the token has no value outside the processor |
| Additional columns | card_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.
| Decision | Recommendation |
|---|---|
| Type | TEXT or BYTEA — encrypted value is longer than the raw PAN |
| Encryption | AES-256-GCM with a key stored in a dedicated KMS (not in the DB) |
| Key rotation | Mandatory — see your KMS documentation |
| Access control | Column-level or row-level security; minimize who can read it |
| Audit log | Every 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}
| Task | How |
|---|---|
| Accept card input | Use processor hosted fields — PAN never reaches your server |
| Validate format | Strip spaces/hyphens → check length (13–19) → Luhn check |
| Store card reference | Store the processor token in card_pan |
| Display to user | Mask: **** **** **** 4242 |
| Identify card type | Store card_brand and card_last_four as separate columns |
| Log a card error | Log token + last 4 only — never the full PAN |
| Check Luhn validity | 15-line function — no library needed |
| What | Allowed to store | Must encrypt |
|---|---|---|
| Processor token | ✅ Yes | No |
| Last 4 digits | ✅ Yes | No |
| Card brand | ✅ Yes | No |
| Full PAN | ⚠️ Only with PCI DSS certification | Yes |
| CVV / CVC | ❌ Never | — |
| Magnetic stripe | ❌ Never | — |
Questions or edge cases not covered here? Open a discussion in the #engineering-standards channel.