Luhn checksum, card-network detection and number / CVV / expiry validation for Visa, Mastercard, American Express, Discover, Diners Club, JCB, Maestro and UnionPay — with explainable, typed results.
TypeScript-first, zero runtime dependencies, and built for regulated/audited contexts: it validates format only, never logs or stores the card number, and makes no network calls.
import { validateCardNumber, validateCard } from '@pametan/card-validator';
validateCardNumber('4242 4242 4242 4242').valid; // true
validateCard({ number: '3782 822463 10005', cvv: '1234', expiry: '11/30' }).valid; // true (Amex)Most card mistakes are caught before a payment processor is ever called: a
mistyped digit (Luhn), the wrong number of digits for the network, a 3-digit
code where Amex wants 4, or an expired date. This library does those checks with
the two awkward networks — Amex (15 digits, 4-digit code, 4-6-5 grouping)
and Discover (scattered IIN ranges incl. 622126–622925) — handled
correctly, and returns a result object you can log or audit.
Scope: this is structural validation, not a BIN lookup and not proof that a card exists or has funds. A valid result means "this looks like a well-formed card number for this network."
npm install @pametan/card-validatorRequires Node 24+. Ships ESM with bundled type declarations.
import { validateCardNumber } from '@pametan/card-validator';
validateCardNumber('6011 0009 9013 9424');
// {
// valid: true,
// network: 'discover',
// luhnValid: true,
// lengthValid: true,
// normalized: '6011000990139424',
// }valid is true only for a recognised network with a passing Luhn checksum and a
valid length. The sub-flags are exposed so you can apply your own policy — for
example, accepting a luhnValid number whose network is 'unknown'.
import { validateCvv, validateExpiry } from '@pametan/card-validator';
validateCvv('1234', 'amex').valid; // true (Amex uses 4 digits)
validateCvv('123', 'visa').valid; // true (others use 3)
// Valid through the last day of the expiry month (industry norm).
validateExpiry('05/26', { now: new Date(2026, 4, 26) }).valid; // true
// graceDays extends the window past end of month:
validateExpiry('05/26', { now: new Date(2026, 5, 3), graceDays: 5 }).valid; // truenow is injectable so expiry checks are deterministic in tests.
import { validateCard } from '@pametan/card-validator';
const result = validateCard({
number: '3782 822463 10005',
cvv: '1234',
expiry: '11/30',
});
// result.valid === true, and result.number / result.cvv / result.expiry hold the detail.The CVV is checked against the network detected from the number, so Amex's 4-digit code is handled automatically.
import { detectNetwork, formatCardNumber, getLast4 } from '@pametan/card-validator';
detectNetwork('2223 0031 2200 3222'); // 'mastercard' (2-series)
formatCardNumber('378282246310005'); // '3782 822463 10005'
getLast4('4242 4242 4242 4242'); // '4242'| Export | Description |
|---|---|
validateCardNumber(pan) |
CardNumberResult — network, Luhn, length, overall. |
detectNetwork(pan) |
One of visa, mastercard, amex, discover, diners, jcb, maestro, unionpay, or unknown. |
validateCvv(cvv, network) |
CvvResult — checks length per network. |
validateExpiry(input, opts?) |
ExpiryResult — MM/YY or MM/YYYY, configurable grace. |
validateCard({ number, cvv, expiry }, opts?) |
CardResult — composite. |
formatCardNumber(pan) / getLast4(pan) |
Display helpers. |
luhnCheck(input) |
The raw Luhn checksum. |
NETWORKS, networkRule(id) |
The underlying network rule table. |
All result and option types are exported.
| Network | IIN / prefix | Lengths | Security code |
|---|---|---|---|
| Visa | 4 |
13, 16, 19 | 3 |
| Mastercard | 51–55, 2221–2720 |
16 | 3 |
| American Express | 34, 37 |
15 | 4 |
| Discover | 6011, 644–649, 65, 622126–622925 |
16, 19 | 3 |
| Diners Club | 300–305, 309, 36, 38, 39 |
14, 16, 19 | 3 |
| JCB | 3528–3589 |
16–19 | 3 |
| Maestro | 5018, 5020, 5038, 5893, 6304, 6759, 6761–6763, 6390, 0604 |
12–19 | 3 |
| UnionPay | 62, 81 |
16–19 | 3 |
Ranges follow ISO/IEC 7812 and each network's published documentation.
Overlap and precedence. Several networks share the 6x IIN space. Discover's
specific ranges — including the co-branded 622126–622925 block — are matched
first, so those BINs route to Discover; UnionPay's broad 62/81 and Maestro's
IIN list only catch what Discover does not. If your acquirer routes a particular
BIN differently, treat the detected network as a hint and confirm with your
processor.
This library never logs, stores or transmits the card number — it only inspects it in memory. To keep it that way in your own code:
- Don't log full card numbers. Use
getLast4()for display, and mask PANs in logs (e.g. with@pametan/pii-redact). - Validation is not a substitute for your payment processor's own checks, and does not reduce or remove your PCI DSS obligations.
Non-goals: real-time BIN lookup, tokenization, storage, and fraud scoring.
npm install
npm run typecheck
npm test # 43 tests: published test PANs + edge cases
npm run build # emit dist/Test card numbers are the standard published synthetic numbers (Stripe / network docs) — they are designed for testing and are not real accounts.
Provided as an engineering aid, not financial, security or compliance advice.
Verify network rules against current documentation for production use. MIT
licensed — see LICENSE.
We're Pametan — a specialist fintech/regtech engineering agency working across UK, US and Canadian rails (FCA · CFPB · FCAC). We build the regulated, audited, in-market version of tools like this: card and payment validation, PCI-scoped checkout flows, and the systems around them.