Apple Pay Web Integration: Merchant Implementation Guide
Production-ready Apple Pay on the Web integration covering merchant validation, payment processing, certificate management, and Safari/Chrome implementation patterns.
Table of Contents
- Apple Pay on the Web Overview
- Prerequisites and Setup
- Merchant Validation Flow
- JavaScript Implementation
- Server-Side Processing
- Certificate Management
- Testing and Debugging
- Common Implementation Issues
Apple Pay on the Web Overview
Apple Pay on the Web allows customers to pay with Apple Pay directly in their browser. It works on:
- Safari on macOS, iOS, iPadOS
- Chrome/Edge/Firefox on iOS 16+ (via Apple Pay JS API)
Key benefits:
- Frictionless checkout - Face ID/Touch ID authentication
- No card entry - Payment info from Apple Wallet
- Tokenization - Secure, single-use tokens
- Trust - Apple's security reputation
Prerequisites and Setup
Requirements
- Apple Developer Account ($99/year)
- Merchant ID - Registered with Apple
- Payment Processing Certificate - For decrypting payment tokens
- Merchant Identity Certificate - For merchant validation
- Domain Verification - Your domain verified with Apple
Creating Certificates
# 1. Create CSR for Payment Processing Certificate openssl req -new -newkey rsa:2048 -nodes \ -keyout payment_processing.key \ -out payment_processing.csr \ -subj "/CN=Merchant ID: merchant.com.yourcompany" # 2. Upload CSR to Apple Developer Portal # 3. Download the .cer file # 4. Convert to PEM openssl x509 -inform DER -in apple_pay.cer -out apple_pay.pem # 5. Create PKCS12 for merchant validation openssl pkcs12 -export \ -inkey merchant_id.key \ -in merchant_id.pem \ -out merchant_id.p12
Merchant Validation Flow
┌─────────────────────────────────────────────────────────────────────┐
│ Apple Pay Payment Flow │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. User clicks Apple Pay button │
│ ┌──────────────┐ │
│ │ Browser │──── canMakePayments() ────▶ Check device support │
│ └──────────────┘ │
│ │
│ 2. Create Apple Pay session │
│ ┌──────────────┐ │
│ │ Browser │──── ApplePaySession() ────▶ Show payment sheet │
│ └──────────────┘ │
│ │
│ 3. Merchant validation │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Browser │────────▶│ Your Server │────────▶│ Apple │ │
│ │ onvalidate │◀────────│ │◀────────│ Servers │ │
│ └──────────────┘ session └──────────────┘ session └─────────────┘ │
│ │
│ 4. User authenticates (Face ID/Touch ID) │
│ │
│ 5. Payment authorized │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Browser │────────▶│ Your Server │────────▶│ Payment │ │
│ │ onpayment │ │ │ │ Processor │ │
│ └──────────────┘ token └──────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
JavaScript Implementation
React Component
// components/payment/ApplePayButton.tsx import { useCallback, useEffect, useState } from 'react'; interface ApplePayButtonProps { amount: number; currency: string; orderId: string; onSuccess: (result: any) => void; onError: (error: any) => void; } declare global { interface Window { ApplePaySession: any; } } export function ApplePayButton({ amount, currency, orderId, onSuccess, onError, }: ApplePayButtonProps) { const [isAvailable, setIsAvailable] = useState(false); useEffect(() => { // Check Apple Pay availability if (window.ApplePaySession && ApplePaySession.canMakePayments()) { setIsAvailable(true); } }, []); const handleApplePay = useCallback(async () => { const paymentRequest = { countryCode: 'US', currencyCode: currency, supportedNetworks: ['visa', 'masterCard', 'amex', 'discover'], merchantCapabilities: ['supports3DS'], total: { label: 'Your Company', amount: amount.toFixed(2), }, }; const session = new ApplePaySession(3, paymentRequest); // Merchant validation session.onvalidatemerchant = async (event: any) => { try { const response = await fetch('/api/apple-pay/validate-merchant', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ validationURL: event.validationURL, }), }); const merchantSession = await response.json(); session.completeMerchantValidation(merchantSession); } catch (error) { console.error('Merchant validation failed:', error); session.abort(); onError(error); } }; // Payment authorized session.onpaymentauthorized = async (event: any) => { try { const response = await fetch('/api/apple-pay/process-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: event.payment.token, orderId, amount, currency, }), }); const result = await response.json(); if (result.success) { session.completePayment(ApplePaySession.STATUS_SUCCESS); onSuccess(result); } else { session.completePayment(ApplePaySession.STATUS_FAILURE); onError(new Error(result.message)); } } catch (error) { session.completePayment(ApplePaySession.STATUS_FAILURE); onError(error); } }; session.oncancel = () => { console.log('Apple Pay cancelled'); }; session.begin(); }, [amount, currency, orderId, onSuccess, onError]); if (!isAvailable) { return null; // Don't show button if Apple Pay not available } return ( <button onClick={handleApplePay} className="apple-pay-button apple-pay-button-black" style={{ width: '100%', height: '48px', borderRadius: '8px', cursor: 'pointer', }} /> ); }
CSS for Apple Pay Button
/* Apple Pay button styles */ .apple-pay-button { -webkit-appearance: -apple-pay-button; -apple-pay-button-type: buy; } .apple-pay-button-black { -apple-pay-button-style: black; } .apple-pay-button-white { -apple-pay-button-style: white; } .apple-pay-button-white-outline { -apple-pay-button-style: white-outline; }
Server-Side Processing
Merchant Validation
// services/applepay/MerchantValidation.ts import https from 'https'; import fs from 'fs'; const MERCHANT_IDENTIFIER = process.env.APPLE_PAY_MERCHANT_ID!; const MERCHANT_DOMAIN = process.env.APPLE_PAY_MERCHANT_DOMAIN!; // Load certificates const merchantIdentityCert = fs.readFileSync('./certs/merchant_id.pem'); const merchantIdentityKey = fs.readFileSync('./certs/merchant_id.key'); export async function validateMerchant(validationURL: string): Promise<any> { return new Promise((resolve, reject) => { const postData = JSON.stringify({ merchantIdentifier: MERCHANT_IDENTIFIER, domainName: MERCHANT_DOMAIN, displayName: 'Your Company', }); const urlObj = new URL(validationURL); const options = { hostname: urlObj.hostname, port: 443, path: urlObj.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData), }, cert: merchantIdentityCert, key: merchantIdentityKey, }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const merchantSession = JSON.parse(data); resolve(merchantSession); } catch (error) { reject(new Error('Invalid merchant session response')); } }); }); req.on('error', (error) => { reject(error); }); req.write(postData); req.end(); }); }
Payment Processing
// services/applepay/PaymentService.ts export class ApplePayService { async processPayment(params: { token: any; orderId: string; amount: number; currency: string; }): Promise<{ success: boolean; transactionId?: string }> { const { token, orderId, amount, currency } = params; // Validate order const order = await this.orderRepo.findById(orderId); if (!order || Math.abs(order.total - amount) > 0.01) { throw new PaymentError('INVALID_ORDER', 'Order validation failed'); } // The token.paymentData contains encrypted payment data // You need to either: // 1. Decrypt it yourself (requires Payment Processing Certificate) // 2. Send it to your payment processor (Stripe, Adyen, etc.) // Example with Stripe: const stripeToken = await stripe.tokens.create({ pk_token: token.paymentData.data, pk_token_instrument_name: token.paymentMethod.displayName, pk_token_payment_network: token.paymentMethod.network, pk_token_transaction_id: token.transactionIdentifier, }); const charge = await stripe.charges.create({ amount: Math.round(amount * 100), currency, source: stripeToken.id, description: `Order #${order.orderNumber}`, metadata: { orderId }, }); await this.paymentRepo.create({ orderId, stripeChargeId: charge.id, amount, currency, status: 'paid', paymentMethod: 'apple_pay', }); await this.orderService.fulfillOrder(orderId); return { success: true, transactionId: charge.id, }; } }
Certificate Management
Rotation Strategy
// utils/certificateManager.ts import fs from 'fs'; import path from 'path'; interface CertificateInfo { cert: Buffer; key: Buffer; expiresAt: Date; } class CertificateManager { private certs: Map<string, CertificateInfo> = new Map(); loadCertificate(name: string): CertificateInfo { const certPath = path.join(process.env.CERT_PATH!, `${name}.pem`); const keyPath = path.join(process.env.CERT_PATH!, `${name}.key`); // Check if already loaded and not expired const existing = this.certs.get(name); if (existing && existing.expiresAt > new Date()) { return existing; } const cert = fs.readFileSync(certPath); const key = fs.readFileSync(keyPath); // Parse expiration from certificate const expiresAt = this.parseExpiration(cert); const certInfo = { cert, key, expiresAt }; this.certs.set(name, certInfo); // Log warning if expiring soon const daysUntilExpiry = (expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24); if (daysUntilExpiry < 30) { console.warn(`Certificate ${name} expires in ${daysUntilExpiry.toFixed(0)} days`); } return certInfo; } } export const certManager = new CertificateManager();
Common Implementation Issues
1. Domain Verification Failing
// Ensure the verification file is served correctly app.get('/.well-known/apple-developer-merchantid-domain-association', (req, res) => { res.sendFile(path.join(__dirname, 'apple-developer-merchantid-domain-association')); });
2. Certificate Chain Issues
// Include full certificate chain const ca = fs.readFileSync('./certs/apple_root_ca.pem'); const options = { cert: merchantCert, key: merchantKey, ca: ca, };
Conclusion
Apple Pay web integration requires:
- Certificate management - PKI setup and rotation
- Merchant validation - Server-to-Apple communication
- Token processing - Send to payment processor
- Browser compatibility - Handle availability checks
Apple Pay dramatically improves checkout conversion on Apple devices. The setup is complex, but the UX improvement is worth it.
Related Articles
Payment Integrations28 min read
Stripe Payment Integration: Production Patterns for React and Node.js
Production Stripe integration with Payment Intents, webhooks, and 3D Secure. Covers subscription billing, error handling, and PCI compliance patterns.
Payment Integrations22 min read
Checkout.com Payment Integration: PCI-Compliant Implementation
Build PCI-compliant payments with Checkout.com Frames.js and Payment Request API. Covers hosted fields, 3D Secure, and webhook implementation.
Security Engineering18 min read
API Security Hardening: A Practitioner's Guide
Secure your APIs with rate limiting, input validation, and CORS configuration. Production-tested checklist covering authentication, encryption, and error handling.
Security Engineering21 min read
Authentication and Authorization in Production Systems
Implement secure JWT authentication with refresh token rotation, RBAC, and OAuth 2.0 flows. Production patterns from healthcare and government systems.