Adyen Payment Integration for Enterprise Applications
Enterprise Adyen integration with Drop-in component and API. Covers checkout configuration, webhook handling, and multi-currency support.
Table of Contents
- Why Adyen for Enterprise
- Architecture for High-Volume Processing
- Server-Side Implementation
- React Drop-in Integration
- Webhook Configuration
- Multi-Currency and Localization
- Risk Management Integration
- Testing and Going Live
Why Adyen for Enterprise
Adyen is the payment platform of choice for enterprise companies like Uber, Spotify, and Microsoft. Unlike Stripe's developer-first approach, Adyen focuses on:
- Single platform for global payments - One integration for 250+ payment methods
- Acquiring and processing - Adyen is both the processor and acquirer
- Advanced risk management - RevenueProtect with ML-based fraud detection
- Enterprise SLAs - Guaranteed uptime and dedicated support
I've implemented Adyen for e-commerce platforms processing €10M+ monthly. This guide covers what the documentation doesn't.
Architecture for High-Volume Processing
┌─────────────────────────────────────────────────────────────────────┐
│ Your E-commerce Platform │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ React UI │────▶│ Payment API │────▶│ Orders DB │ │
│ │ (Drop-in) │ │ (Node.js) │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
│ │ │ │
│ │ /sessions │ /payments │
│ │ │ /payments/details │
└─────────┼────────────────────┼───────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ Adyen Platform │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ Checkout API │ │ Revenue │ │ Notification │ │
│ │ │ │ Protect │ │ Webhooks │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Server-Side Implementation
Configuration
// config/adyen.ts import { Client, Config, CheckoutAPI } from '@adyen/api-library'; const config = new Config(); config.apiKey = process.env.ADYEN_API_KEY!; config.merchantAccount = process.env.ADYEN_MERCHANT_ACCOUNT!; // Environment: TEST or LIVE const environment = process.env.NODE_ENV === 'production' ? 'LIVE' : 'TEST'; // For LIVE, you need the live endpoint prefix if (environment === 'LIVE') { config.environment = 'LIVE'; // Your live endpoint prefix from Adyen dashboard config.liveEndpointUrlPrefix = process.env.ADYEN_LIVE_PREFIX; } else { config.environment = 'TEST'; } const client = new Client({ config }); export const checkout = new CheckoutAPI(client); export const ADYEN_CONFIG = { merchantAccount: process.env.ADYEN_MERCHANT_ACCOUNT!, clientKey: process.env.ADYEN_CLIENT_KEY!, environment: environment.toLowerCase() as 'test' | 'live', hmacKey: process.env.ADYEN_HMAC_KEY!, };
Creating Payment Sessions
// services/adyen/PaymentService.ts import { checkout, ADYEN_CONFIG } from '@/config/adyen'; import { CreateCheckoutSessionRequest } from '@adyen/api-library/lib/src/typings/checkout/createCheckoutSessionRequest'; interface CreateSessionParams { orderId: string; amount: number; currency: string; customerId?: string; customerEmail?: string; countryCode: string; returnUrl: string; lineItems?: Array<{ id: string; description: string; quantity: number; amountIncludingTax: number; }>; } export class AdyenPaymentService { async createSession(params: CreateSessionParams): Promise<{ sessionId: string; sessionData: string; }> { const { orderId, amount, currency, customerId, customerEmail, countryCode, returnUrl, lineItems, } = params; // Validate order const order = await this.orderRepo.findById(orderId); if (!order || order.totalAmountCents !== amount) { throw new PaymentError('INVALID_ORDER', 'Order validation failed'); } const sessionRequest: CreateCheckoutSessionRequest = { merchantAccount: ADYEN_CONFIG.merchantAccount, amount: { value: amount, currency: currency.toUpperCase(), }, reference: orderId, returnUrl: `${returnUrl}?orderId=${orderId}`, countryCode, shopperEmail: customerEmail, shopperReference: customerId, // Enable specific payment methods allowedPaymentMethods: [ 'scheme', // Cards 'applepay', 'googlepay', 'paypal', 'klarna', 'klarna_paynow', 'klarna_account', ], // Block high-risk methods if needed blockedPaymentMethods: ['paysafecard'], // Line items for payment methods like Klarna lineItems: lineItems?.map(item => ({ id: item.id, description: item.description, quantity: item.quantity, amountIncludingTax: item.amountIncludingTax, })), // Capture immediately or authorize only captureDelayHours: 0, // Immediate capture // Store payment method for future use storePaymentMethod: !!customerId, recurringProcessingModel: customerId ? 'CardOnFile' : undefined, // Risk data shopperIP: params.shopperIP, shopperInteraction: 'Ecommerce', // 3D Secure settings authenticationData: { threeDSRequestData: { nativeThreeDS: 'preferred', }, }, // Additional data for risk engine additionalData: { 'riskdata.basket.item1.itemID': lineItems?.[0]?.id, 'riskdata.basket.item1.productTitle': lineItems?.[0]?.description, }, // Channel channel: 'Web', // Expiry: sessions expire after 1 hour expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), }; try { const response = await checkout.PaymentsApi.sessions(sessionRequest); // Store session reference await this.paymentRepo.create({ orderId, adyenSessionId: response.id, amount, currency, status: 'pending', }); return { sessionId: response.id!, sessionData: response.sessionData!, }; } catch (error) { console.error('Adyen session creation failed:', error); throw new PaymentError('SESSION_FAILED', 'Could not create payment session'); } } }
React Drop-in Integration
Setting Up the Drop-in Component
// components/payment/AdyenCheckout.tsx import { useEffect, useRef, useState } from 'react'; import AdyenCheckout from '@adyen/adyen-web'; import '@adyen/adyen-web/dist/adyen.css'; interface AdyenCheckoutProps { sessionId: string; sessionData: string; clientKey: string; environment: 'test' | 'live'; onPaymentCompleted: (result: any) => void; onError: (error: any) => void; amount: { value: number; currency: string }; countryCode: string; locale?: string; } export function AdyenCheckoutComponent({ sessionId, sessionData, clientKey, environment, onPaymentCompleted, onError, amount, countryCode, locale = 'en-US', }: AdyenCheckoutProps) { const paymentContainer = useRef<HTMLDivElement>(null); const [checkout, setCheckout] = useState<any>(null); useEffect(() => { const initCheckout = async () => { try { const adyenCheckout = await AdyenCheckout({ environment, clientKey, session: { id: sessionId, sessionData, }, // Localization locale, countryCode, // Styling showPayButton: true, paymentMethodsConfiguration: { card: { hasHolderName: true, holderNameRequired: true, enableStoreDetails: true, name: 'Credit or Debit Card', styles: { base: { fontSize: '16px', fontFamily: 'system-ui, sans-serif', }, }, }, applepay: { amount: { value: amount.value, currency: amount.currency, }, countryCode, }, googlepay: { amount: { value: amount.value, currency: amount.currency, }, countryCode, environment: environment === 'live' ? 'PRODUCTION' : 'TEST', }, paypal: { amount: { value: amount.value, currency: amount.currency, }, environment: environment === 'live' ? 'live' : 'test', countryCode, intent: 'capture', }, }, // Analytics analytics: { enabled: true, }, // Event handlers onPaymentCompleted: (result: any, component: any) => { console.log('Payment completed:', result); onPaymentCompleted(result); }, onError: (error: any, component: any) => { console.error('Payment error:', error); onError(error); }, // Additional action handling (3DS, redirects) onAdditionalDetails: async (state: any, component: any) => { // Handle additional details if needed console.log('Additional details:', state); }, }); setCheckout(adyenCheckout); // Mount Drop-in if (paymentContainer.current) { adyenCheckout .create('dropin') .mount(paymentContainer.current); } } catch (error) { console.error('Adyen initialization failed:', error); onError(error); } }; initCheckout(); return () => { checkout?.unmount(); }; }, [sessionId, sessionData]); return ( <div className="adyen-checkout-container"> <div ref={paymentContainer} /> <style jsx>{` .adyen-checkout-container { min-height: 300px; } :global(.adyen-checkout__payment-method) { border: 1px solid #e5e7eb; border-radius: 8px; margin-bottom: 8px; } :global(.adyen-checkout__payment-method--selected) { border-color: #0070f3; } :global(.adyen-checkout__button--pay) { background: #0070f3; border-radius: 8px; height: 48px; font-weight: 600; } `}</style> </div> ); }
Webhook Configuration
HMAC Signature Verification
// middleware/adyenWebhook.ts import crypto from 'crypto'; import { ADYEN_CONFIG } from '@/config/adyen'; export function verifyAdyenWebhook( body: string, hmacSignature: string ): boolean { const hmacKey = ADYEN_CONFIG.hmacKey; // Calculate expected signature const expectedSignature = crypto .createHmac('sha256', Buffer.from(hmacKey, 'hex')) .update(body, 'utf8') .digest('base64'); return crypto.timingSafeEqual( Buffer.from(hmacSignature), Buffer.from(expectedSignature) ); } export function adyenWebhookMiddleware(req: Request, res: Response, next: NextFunction) { const hmacSignature = req.headers['x-adyen-hmac'] as string; if (!hmacSignature) { return res.status(400).json({ error: 'Missing HMAC signature' }); } // Adyen sends notifications as JSON array const body = JSON.stringify(req.body); if (!verifyAdyenWebhook(body, hmacSignature)) { return res.status(401).json({ error: 'Invalid HMAC signature' }); } next(); }
Webhook Handler
// services/adyen/WebhookService.ts import { NotificationRequestItem } from '@adyen/api-library/lib/src/typings/notification/notificationRequestItem'; export class AdyenWebhookService { async handleNotification(notification: NotificationRequestItem): Promise<void> { const { eventCode, merchantReference, pspReference, success } = notification; // Idempotency check const existing = await this.webhookRepo.findByPspReference(pspReference); if (existing) { console.log(`Notification ${pspReference} already processed`); return; } await this.webhookRepo.create({ pspReference, eventCode, merchantReference, success: success === 'true', data: notification, }); switch (eventCode) { case 'AUTHORISATION': await this.handleAuthorisation(notification); break; case 'CAPTURE': await this.handleCapture(notification); break; case 'CANCELLATION': await this.handleCancellation(notification); break; case 'REFUND': await this.handleRefund(notification); break; case 'CHARGEBACK': await this.handleChargeback(notification); break; case 'REPORT_AVAILABLE': await this.handleReport(notification); break; } } private async handleAuthorisation(notification: NotificationRequestItem): Promise<void> { const orderId = notification.merchantReference; const isSuccess = notification.success === 'true'; await this.paymentRepo.updateByOrderId(orderId, { status: isSuccess ? 'authorized' : 'failed', pspReference: notification.pspReference, paymentMethod: notification.paymentMethod, cardSummary: notification.additionalData?.cardSummary, }); if (isSuccess) { await this.orderService.confirmOrder(orderId); await this.notificationService.sendOrderConfirmation(orderId); } else { const reason = notification.reason || 'Payment declined'; await this.notificationService.sendPaymentFailed(orderId, reason); } } }
Multi-Currency and Localization
// utils/adyenCurrency.ts // Adyen uses minor units (cents) for most currencies // But some currencies don't have minor units const ZERO_DECIMAL_CURRENCIES = [ 'JPY', 'KRW', 'VND', 'IDR', 'CLP', 'PYG', 'UGX', 'GNF', 'RWF', 'DJF', 'KMF', 'XAF', 'XOF', 'XPF', ]; const THREE_DECIMAL_CURRENCIES = ['BHD', 'KWD', 'OMR', 'JOD']; export function toAdyenAmount(amount: number, currency: string): number { const upperCurrency = currency.toUpperCase(); if (ZERO_DECIMAL_CURRENCIES.includes(upperCurrency)) { return Math.round(amount); } if (THREE_DECIMAL_CURRENCIES.includes(upperCurrency)) { return Math.round(amount * 1000); } return Math.round(amount * 100); } export function fromAdyenAmount(amount: number, currency: string): number { const upperCurrency = currency.toUpperCase(); if (ZERO_DECIMAL_CURRENCIES.includes(upperCurrency)) { return amount; } if (THREE_DECIMAL_CURRENCIES.includes(upperCurrency)) { return amount / 1000; } return amount / 100; }
Risk Management Integration
// Adding risk data to payment requests const riskData = { // Device fingerprint from Adyen's client-side script 'riskdata.deliveryMethod': 'express', 'riskdata.basket.numberOfItems': cartItems.length.toString(), 'riskdata.promotions.promotion1.promotionCode': promoCode || '', // Customer risk data 'riskdata.shopperAccountCreationDate': customer.createdAt.toISOString(), 'riskdata.shopperAccountChangeDate': customer.updatedAt.toISOString(), 'riskdata.numberOfPurchases': customer.orderCount.toString(), };
Conclusion
Adyen is powerful but complex. Key takeaways:
- Use Sessions API - Modern approach with built-in 3DS handling
- HMAC verification - Always verify webhook signatures
- Handle all event codes - Not just AUTHORISATION
- Currency handling - Different currencies have different decimal places
- Risk data - Feed RevenueProtect with quality data
The investment in proper Adyen integration pays off at scale.
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.
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.
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.
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.
Payment Integrations24 min read
PayPal Payment Integration: Production Implementation Guide
Integrate PayPal with Orders API and Smart Buttons. Covers webhook setup, subscription payments, and real error handling patterns from production.