Loading learning content...
Every organization that stores, processes, or transmits credit card data must comply with the Payment Card Industry Data Security Standard (PCI DSS). Non-compliance isn't just a security risk—it carries legal and financial consequences that can be existential:
Compliance is not optional. It's a condition of doing business with payment cards. This page teaches you to design systems that achieve compliance efficiently while minimizing the scope and cost of your compliance obligations.
By the end of this page, you will understand PCI DSS requirements and levels, scope reduction strategies (tokenization, hosted fields), the 12 requirements of PCI DSS, implementation patterns for each requirement, compliance validation and ongoing obligations, and related regulations (PSD2, GDPR, state laws).
PCI DSS is a set of security standards created by the Payment Card Industry Security Standards Council (PCI SSC), founded by Visa, Mastercard, American Express, Discover, and JCB. Any entity that handles cardholder data must comply.
What is "Cardholder Data"?
| Data Element | Description | Storage Allowed? | Must Encrypt? |
|---|---|---|---|
| Primary Account Number (PAN) | The 16-digit card number | Yes (protected) | Yes |
| Cardholder Name | Name on the card | Yes | Recommended |
| Expiration Date | Month/year card expires | Yes | Recommended |
| Service Code | 3-digit code on magnetic stripe | Yes | Recommended |
| Full Track Data | Complete magnetic stripe data | NEVER | N/A - don't store |
| CVV/CVC | 3-4 digit security code | NEVER | N/A - don't store |
| PIN/PIN Block | Personal identification number | NEVER | N/A - don't store |
Compliance Levels:
PCI DSS compliance requirements scale with your transaction volume:
| Level | Transaction Volume | Requirements | Validation |
|---|---|---|---|
| Level 1 | 6 million transactions/year | All 12 requirements, quarterly scans | Annual on-site audit (QSA) |
| Level 2 | 1-6 million transactions/year | All 12 requirements, quarterly scans | Annual SAQ, quarterly scans |
| Level 3 | 20,000-1 million e-commerce/year | All 12 requirements, quarterly scans | Annual SAQ, quarterly scans |
| Level 4 | < 20,000 e-commerce, <1M other | All 12 requirements | Annual SAQ (simplified) |
If you experience a data breach, you're automatically escalated to Level 1 for at least one year, regardless of transaction volume. This means mandatory on-site audits by a Qualified Security Assessor (QSA)—significantly more expensive than self-assessment.
PCI DSS 4.0 organizes security requirements into six goals with twelve requirements. Each requirement has multiple sub-requirements—over 300 in total.
PCI DSS 4.0 (released March 2022, mandatory March 2025) adds: customized approach for meeting requirements, enhanced authentication requirements, expanded scope for cloud/service providers, and new requirements for targeted risk analysis. Plan compliance updates early.
The most effective compliance strategy is reducing the scope of systems that handle cardholder data. Every system that touches card data must be secured, audited, and maintained to PCI standards. Fewer systems = lower cost and complexity.
Key Scope Reduction Techniques:
| SAQ Type | Scope | Requirements | Typical Use Case |
|---|---|---|---|
| SAQ A | 47 questions | Full redirect or hosted fields (iFrame) | Most e-commerce sites |
| SAQ A-EP | 191 questions | Your JS on page submits to gateway | Custom checkout with gateway.js |
| SAQ B | 41 questions | Imprint or dial-out terminal only | Small retail |
| SAQ B-IP | 83 questions | IP-connected terminal | Retail POS |
| SAQ C | 160 questions | Payment app on internet-connected device | Virtual terminal |
| SAQ C-VT | 79 questions | Virtual terminal only | Call center |
| SAQ D-Merchant | 329 questions | All other merchants | Custom payment handling |
| SAQ D-SP | 329+ questions | Service providers storing CHD | Payment processors |
SAQ A has 47 questions. SAQ D has 329. Using hosted fields or full redirect isn't just about security—it's about reducing compliance effort by 85%. Design for SAQ A from the start.
Let's look at concrete implementation patterns that satisfy PCI DSS requirements while keeping scope minimal.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
// Pattern 1: Hosted Fields Integration (SAQ A)// Card data never touches your code - gateway controls input fields import { loadStripe } from '@stripe/stripe-js';import { Elements, CardElement, useStripe } from '@stripe/react-stripe-js'; // Initialize Stripe (loads from their CDN, not your bundle)const stripePromise = loadStripe('pk_live_...'); function CheckoutForm() { const stripe = useStripe(); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!stripe) return; // createPaymentMethod sends card data directly to Stripe // Your server never sees the card number const { paymentMethod, error } = await stripe.createPaymentMethod({ type: 'card', card: cardElement, // Reference to Stripe's hosted element billing_details: { name: 'Customer Name', }, }); if (error) { console.error(error); return; } // Only send the token to your server const response = await fetch('/api/process-payment', { method: 'POST', body: JSON.stringify({ paymentMethodId: paymentMethod.id, // Token, not card number! amount: 5000, currency: 'usd', }), }); }; return ( <form onSubmit={handleSubmit}> {/* CardElement is rendered by Stripe in an iFrame */} {/* Your page never has access to card number */} <CardElement options={{ style: { base: { fontSize: '16px', color: '#424770', }, }, }} /> <button type="submit">Pay $50</button> </form> );} // Wrap with Elements providerfunction Checkout() { return ( <Elements stripe={stripePromise}> <CheckoutForm /> </Elements> );}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// Pattern 2: Backend handling tokens (never card data) interface ProcessPaymentRequest { paymentMethodId: string; // Token from frontend amount: number; currency: string; customerId: string;} class PaymentController { async processPayment(req: Request): Promise<Response> { const body = req.body as ProcessPaymentRequest; // Validate token format (should be token, not card number!) if (!this.isValidTokenFormat(body.paymentMethodId)) { throw new ValidationError('Invalid payment method format'); } // Double-check we're not receiving card data if (this.looksLikeCardNumber(body.paymentMethodId)) { // Log security alert - someone is sending card data! this.securityLogger.critical('Raw card data received in API'); throw new SecurityError('Invalid request format'); } // Process with gateway using token const result = await this.paymentService.charge({ paymentMethodToken: body.paymentMethodId, amount: body.amount, currency: body.currency, customerId: body.customerId, }); return Response.json(result); } private isValidTokenFormat(value: string): boolean { // Stripe: pm_*, Adyen: stored-payment-method-* return value.startsWith('pm_') || value.startsWith('tok_') || value.startsWith('stored-'); } private looksLikeCardNumber(value: string): boolean { // Check if value looks like a card number const digitsOnly = value.replace(/\D/g, ''); return digitsOnly.length >= 13 && digitsOnly.length <= 19 && this.passesLuhnCheck(digitsOnly); } private passesLuhnCheck(number: string): boolean { // Luhn algorithm for card number validation let sum = 0; let isEven = false; for (let i = number.length - 1; i >= 0; i--) { let digit = parseInt(number[i], 10); if (isEven) { digit *= 2; if (digit > 9) digit -= 9; } sum += digit; isEven = !isEven; } return sum % 10 === 0; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
// Pattern 3: Comprehensive Audit Logging (Requirement 10) interface AuditLog { timestamp: Date; eventType: string; userId: string; resourceType: string; resourceId: string; action: string; outcome: 'success' | 'failure'; ipAddress: string; userAgent: string; details: Record<string, unknown>;} class AuditLogger { constructor( private storage: AuditStorage, private config: AuditConfig ) {} async log(event: AuditLog): Promise<void> { // Ensure no sensitive data in logs const sanitized = this.sanitize(event); // Write to immutable audit log await this.storage.write(sanitized); // Real-time alerting for suspicious activity if (this.isSuspicious(event)) { await this.alertSecurityTeam(event); } } private sanitize(event: AuditLog): AuditLog { const sanitized = { ...event }; // Remove any accidental PAN data sanitized.details = this.maskSensitiveData(event.details); return sanitized; } private maskSensitiveData(data: Record<string, unknown>): Record<string, unknown> { const masked = { ...data }; for (const [key, value] of Object.entries(masked)) { if (typeof value === 'string') { // Mask anything that looks like a card number if (this.looksLikeCardNumber(value)) { masked[key] = this.mask(value); } // Mask CVV-like values if (/^\d{3,4}$/.test(value) && key.toLowerCase().includes('cv')) { masked[key] = '***'; } } else if (typeof value === 'object') { masked[key] = this.maskSensitiveData(value as Record<string, unknown>); } } return masked; } private mask(cardNumber: string): string { // Show only last 4 digits const digitsOnly = cardNumber.replace(/\D/g, ''); const last4 = digitsOnly.slice(-4); return `****${last4}`; } private isSuspicious(event: AuditLog): boolean { // Detect suspicious patterns const suspiciousPatterns = [ // Multiple failed auth attempts event.eventType === 'authentication' && event.outcome === 'failure', // Bulk data access event.action === 'export' && event.resourceType === 'transactions', // Admin access outside business hours event.resourceType === 'admin' && !this.isBusinessHours(), // Access from new location event.eventType === 'access' && !this.isKnownLocation(event.ipAddress), ]; return suspiciousPatterns.some(p => p); }} // Requirement 10.2: Key events that MUST be loggedconst REQUIRED_AUDIT_EVENTS = [ 'user_access_cardholder_data', 'admin_action', 'access_control_change', 'authentication_attempt', 'audit_log_access', 'security_event', 'initialization_of_audit_logs', 'creation_modification_deletion_of_accounts',];PCI DSS requires audit logs be retained for at least 1 year, with the most recent 3 months immediately available. Implement log rotation, compression, and archival that meets these requirements.
Network segmentation isolates the Cardholder Data Environment (CDE) from other systems, reducing attack surface and compliance scope. Only systems that absolutely need access to card data should be in the CDE.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
# Kubernetes Network Policy for CDE Isolation apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: payment-service-policy namespace: cde # Dedicated namespace for CDEspec: podSelector: matchLabels: app: payment-service policyTypes: - Ingress - Egress # Only allow ingress from API gateway ingress: - from: - namespaceSelector: matchLabels: name: api-gateway podSelector: matchLabels: app: api-gateway ports: - protocol: TCP port: 443 # Only allow egress to: # - Payment gateway (external) # - Token database (internal) # - DNS egress: - to: - podSelector: matchLabels: app: token-database ports: - protocol: TCP port: 5432 # Payment gateway IPs (whitelist) - to: - ipBlock: cidr: 54.200.0.0/16 # Stripe's IP range ports: - protocol: TCP port: 443 # DNS - to: - port: 53 protocol: UDP - port: 53 protocol: TCP ---# Deny all other traffic to CDE namespaceapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny-cde namespace: cdespec: podSelector: {} policyTypes: - Ingress - EgressRequirement 11.3.4 requires testing segmentation controls every 6 months (or after changes). Automate this: run penetration tests that attempt to access CDE from out-of-scope systems. Any success is a segmentation failure.
PCI DSS compliance isn't a one-time achievement—it requires ongoing validation and maintenance. Here's the compliance lifecycle:
| Activity | Frequency | Who Performs | Purpose |
|---|---|---|---|
| Vulnerability Scans (ASV) | Quarterly | Approved Scanning Vendor | Detect vulnerabilities in external-facing systems |
| Internal Vulnerability Scans | Quarterly | Internal team or vendor | Detect internal vulnerabilities |
| Penetration Testing | Annually + after major changes | Qualified tester | Test real-world attack scenarios |
| Segmentation Testing | Every 6 months | Qualified tester | Verify network segmentation effectiveness |
| SAQ Completion | Annually | Internal / QSA | Self-assessment of compliance |
| Security Awareness Training | Annually | Internal | Educate staff on security policies |
| Risk Assessment | Annually | Internal | Identify and prioritize risks |
| Policy Review | Annually | Internal | Update security policies |
| Log Review | Daily | Internal / SIEM | Detect suspicious activity |
| Wireless Scanning | Quarterly | Internal | Detect rogue wireless access points |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
// Automating PCI Compliance Checks interface ComplianceCheckResult { checkId: string; requirement: string; status: 'pass' | 'fail' | 'warning'; details: string; evidence: string[]; remediation?: string;} class PCIComplianceChecker { async runDailyChecks(): Promise<ComplianceCheckResult[]> { const results: ComplianceCheckResult[] = []; // Requirement 10: Verify log collection results.push(await this.checkLogCollection()); // Requirement 2: Check for default credentials results.push(await this.checkDefaultCredentials()); // Requirement 6: Check for missing patches results.push(await this.checkSecurityPatches()); // Requirement 8: Check for inactive user accounts results.push(await this.checkInactiveAccounts()); // Alert on failures const failures = results.filter(r => r.status === 'fail'); if (failures.length > 0) { await this.alertSecurityTeam(failures); } return results; } private async checkLogCollection(): Promise<ComplianceCheckResult> { // Verify all CDE systems are sending logs const cdeSystems = await this.cdeInventory.getSystems(); const loggingSystems = await this.siemClient.getActiveSources(); const missingLogs = cdeSystems.filter( sys => !loggingSystems.includes(sys.id) ); return { checkId: 'PCI-10.2', requirement: 'Audit logging for all CDE systems', status: missingLogs.length === 0 ? 'pass' : 'fail', details: missingLogs.length === 0 ? 'All CDE systems logging to SIEM' : `${missingLogs.length} systems not sending logs`, evidence: loggingSystems, remediation: missingLogs.length > 0 ? `Configure logging for: ${missingLogs.map(s => s.name).join(', ')}` : undefined, }; } private async checkDefaultCredentials(): Promise<ComplianceCheckResult> { // Check common default credential patterns const defaultCreds = [ { service: 'postgres', user: 'postgres', pass: 'postgres' }, { service: 'redis', user: '', pass: '' }, { service: 'admin', user: 'admin', pass: 'admin' }, ]; const issues: string[] = []; for (const cred of defaultCreds) { if (await this.tryCredential(cred)) { issues.push(`${cred.service} using default credentials`); } } return { checkId: 'PCI-2.1', requirement: 'Remove vendor-supplied defaults', status: issues.length === 0 ? 'pass' : 'fail', details: issues.length === 0 ? 'No default credentials found' : `Found ${issues.length} default credential issues`, evidence: issues, remediation: issues.length > 0 ? 'Change all default credentials immediately' : undefined, }; } private async checkSecurityPatches(): Promise<ComplianceCheckResult> { // Query patch management system for missing critical patches const missingPatches = await this.patchManager.getCriticalMissing({ maxAgeDays: 30, // PCI requires critical patches within 30 days systems: await this.cdeInventory.getSystems(), }); return { checkId: 'PCI-6.3.3', requirement: 'Install critical patches within 30 days', status: missingPatches.length === 0 ? 'pass' : 'fail', details: missingPatches.length === 0 ? 'All critical patches applied' : `${missingPatches.length} critical patches overdue`, evidence: missingPatches.map(p => p.cveId), remediation: missingPatches.length > 0 ? 'Apply patches immediately: ' + missingPatches.map(p => p.cveId).join(', ') : undefined, }; } private async checkInactiveAccounts(): Promise<ComplianceCheckResult> { // Find accounts inactive for 90+ days const inactiveAccounts = await this.identityProvider.getInactiveUsers({ inactiveDays: 90, scope: 'cde', }); return { checkId: 'PCI-8.1.4', requirement: 'Remove/disable inactive accounts within 90 days', status: inactiveAccounts.length === 0 ? 'pass' : 'fail', details: inactiveAccounts.length === 0 ? 'No inactive accounts found' : `${inactiveAccounts.length} inactive accounts require review`, evidence: inactiveAccounts.map(a => a.username), remediation: inactiveAccounts.length > 0 ? 'Disable or remove inactive accounts' : undefined, }; }}During QSA audits, you must provide evidence for every requirement. Automate evidence collection: screenshots of configurations, exports of policies, scan reports, training completion records. Manual evidence gathering during audit is stressful and error-prone.
PCI DSS is just one of many regulations affecting payment systems. Depending on your geography and business model, you may also need to comply with:
| Regulation | Region | Scope | Key Requirements |
|---|---|---|---|
| PSD2/SCA | European Union | All electronic payments | Strong Customer Authentication (2FA) for most transactions |
| GDPR | EU / Global | Personal data of EU residents | Consent, data minimization, right to deletion, breach notification |
| CCPA/CPRA | California | Personal data of CA residents | Disclosure, opt-out of sale, deletion rights |
| Open Banking | UK, EU, others | Banks and TPPs | API access to account data, payment initiation |
| Money Transmitter Laws | US (state-by-state) | Money movement/held funds | State licenses, bonding, reporting |
| AML/KYC | Global | Financial institutions | Customer identification, transaction monitoring, suspicious activity reporting |
| BSA | United States | Financial institutions | Record keeping, reporting, AML programs |
Regulatory compliance is complex and varies by jurisdiction. This overview is not legal advice. Consult qualified legal counsel familiar with payment regulations in your operating jurisdictions.
Compliance is a foundation, not an afterthought. Design for compliance from day one. Let's consolidate the key principles:
Module Complete:
You've now covered the complete lifecycle of payment system design: requirements analysis, gateway integration, idempotency guarantees, fraud detection, transaction reconciliation, and regulatory compliance. These components together form the foundation for building payment infrastructure that can process billions securely and reliably.
Congratulations! You now have the knowledge to design payment systems at scale—from handling the first transaction to passing security audits. This is the foundation used by companies processing trillions of dollars annually.