Payment Integrations

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.

Khalid Aboubakr
22 min read
Checkout ComPayment GatewayPci ComplianceReactNodejsFramesWebhooksPayment Request Api

Table of Contents

  1. Why Checkout.com
  2. PCI Compliance with Frames
  3. Server-Side Payment Processing
  4. React Frames Integration
  5. 3D Secure Implementation
  6. Webhook Processing
  7. Production Deployment

Why Checkout.com

Checkout.com has carved a niche between Stripe's developer experience and Adyen's enterprise features:

  • Transparent pricing - Interchange++ for high-volume merchants
  • Global acquiring - Direct connections in 50+ countries
  • Modern API - REST with good documentation
  • Flexible integration - Hosted, Frames, or direct API

I've used Checkout.com for marketplaces needing split payments and high-volume e-commerce requiring interchange++ pricing.

PCI Compliance with Frames

Frames.js is Checkout.com's answer to Stripe Elements—a client-side library that tokenizes card details without your server ever seeing them.

How Frames Works

┌─────────────────────────────────────────────────────────────────┐
│                       Your Website                               │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                    Frames.js iframes                     │   │
│  │  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │   │
│  │  │ Card Number │ │ Expiry Date │ │     CVV     │       │   │
│  │  │  (iframe)   │ │  (iframe)   │ │  (iframe)   │       │   │
│  │  └──────┬──────┘ └──────┬──────┘ └──────┬──────┘       │   │
│  │         │               │               │               │   │
│  └─────────┼───────────────┼───────────────┼───────────────┘   │
│            │               │               │                    │
│            └───────────────┼───────────────┘                    │
│                            │                                     │
│                            ▼                                     │
│                     Token returned                               │
│                   to your JavaScript                             │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
                             │
                             │ Token sent to your server
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Your Server                                 │
│                                                                  │
│  Token used with Checkout.com API                               │
│  (You never see card details)                                   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Server-Side Payment Processing

Configuration

// config/checkout.ts import { Checkout } from 'checkout-sdk-node'; const checkout = new Checkout(process.env.CHECKOUT_SECRET_KEY!, { pk: process.env.CHECKOUT_PUBLIC_KEY!, environment: process.env.NODE_ENV === 'production' ? Checkout.Environment.Production : Checkout.Environment.Sandbox, }); export { checkout }; export const CHECKOUT_CONFIG = { publicKey: process.env.CHECKOUT_PUBLIC_KEY!, environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox', webhookSecret: process.env.CHECKOUT_WEBHOOK_SECRET!, };

Payment Request

// services/checkout/PaymentService.ts import { checkout } from '@/config/checkout'; interface ProcessPaymentParams { token: string; orderId: string; amount: number; currency: string; customerEmail: string; billingAddress?: { addressLine1: string; city: string; state?: string; zip: string; country: string; }; capture: boolean; saveCard?: boolean; customerId?: string; } export class CheckoutPaymentService { async processPayment(params: ProcessPaymentParams): Promise<{ paymentId: string; status: string; redirectUrl?: string; requiresAction: boolean; }> { const { token, orderId, amount, currency, customerEmail, billingAddress, capture, saveCard, customerId, } = params; // Validate order const order = await this.orderRepo.findById(orderId); if (!order || order.totalAmountCents !== amount) { throw new PaymentError('INVALID_ORDER', 'Order validation failed'); } try { const paymentRequest: any = { source: { type: 'token', token, }, amount, currency: currency.toUpperCase(), reference: orderId, capture, customer: { email: customerEmail, }, billing: billingAddress ? { address: { address_line1: billingAddress.addressLine1, city: billingAddress.city, state: billingAddress.state, zip: billingAddress.zip, country: billingAddress.country, }, } : undefined, '3ds': { enabled: true, attempt_n3d: false, // Fail if 3DS not available }, metadata: { orderId, customerId: customerId || 'guest', }, success_url: `${process.env.APP_URL}/checkout/success?orderId=${orderId}`, failure_url: `${process.env.APP_URL}/checkout/failure?orderId=${orderId}`, }; // Store card for future use if (saveCard && customerId) { paymentRequest.source.store_for_future_use = true; paymentRequest.customer.id = customerId; } const payment = await checkout.payments.request(paymentRequest); // Store payment record await this.paymentRepo.create({ orderId, checkoutPaymentId: payment.id, amount, currency, status: payment.status, requiresRedirect: payment.status === 'Pending', redirectUrl: payment._links?.redirect?.href, }); // Check if 3DS redirect is required if (payment.status === 'Pending' && payment._links?.redirect) { return { paymentId: payment.id, status: 'pending_3ds', redirectUrl: payment._links.redirect.href, requiresAction: true, }; } // Payment completed without 3DS if (payment.status === 'Authorized' || payment.status === 'Captured') { await this.orderService.confirmOrder(orderId); return { paymentId: payment.id, status: payment.status.toLowerCase(), requiresAction: false, }; } throw new PaymentError('PAYMENT_FAILED', `Payment status: ${payment.status}`); } catch (error: any) { console.error('Checkout.com payment failed:', error); if (error.http_code === 422) { throw new PaymentError('CARD_DECLINED', 'Your card was declined'); } throw new PaymentError('PAYMENT_ERROR', 'Payment processing failed'); } } }

React Frames Integration

// components/payment/CheckoutFrames.tsx import { useEffect, useRef, useState, useCallback } from 'react'; interface CheckoutFramesProps { publicKey: string; onTokenized: (token: string) => void; onError: (error: any) => void; onCardValidationChanged: (isValid: boolean) => void; } declare global { interface Window { Frames: any; } } export function CheckoutFrames({ publicKey, onTokenized, onError, onCardValidationChanged, }: CheckoutFramesProps) { const [isLoaded, setIsLoaded] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const framesRef = useRef<any>(null); useEffect(() => { // Load Frames.js script const script = document.createElement('script'); script.src = 'https://cdn.checkout.com/js/framesv2.min.js'; script.async = true; script.onload = () => setIsLoaded(true); document.body.appendChild(script); return () => { document.body.removeChild(script); }; }, []); useEffect(() => { if (!isLoaded || !window.Frames) return; window.Frames.init({ publicKey, style: { base: { fontSize: '16px', fontFamily: 'system-ui, -apple-system, sans-serif', color: '#1a1a1a', }, invalid: { color: '#dc2626', }, placeholder: { base: { color: '#9ca3af', }, }, }, localization: { cardNumberPlaceholder: 'Card number', expiryMonthPlaceholder: 'MM', expiryYearPlaceholder: 'YY', cvvPlaceholder: 'CVV', }, }); window.Frames.addEventHandler( window.Frames.Events.CARD_VALIDATION_CHANGED, (event: any) => { onCardValidationChanged(event.isValid); } ); window.Frames.addEventHandler( window.Frames.Events.CARD_TOKENIZED, (event: any) => { setIsSubmitting(false); onTokenized(event.token); } ); window.Frames.addEventHandler( window.Frames.Events.CARD_TOKENIZATION_FAILED, (error: any) => { setIsSubmitting(false); onError(error); } ); framesRef.current = window.Frames; return () => { // Cleanup if needed }; }, [isLoaded, publicKey]); const handleSubmit = useCallback(async () => { if (!framesRef.current || isSubmitting) return; setIsSubmitting(true); try { await framesRef.current.submitCard(); } catch (error) { setIsSubmitting(false); onError(error); } }, [isSubmitting, onError]); if (!isLoaded) { return <div className="animate-pulse h-48 bg-gray-100 rounded-lg" />; } return ( <div className="space-y-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Card Number </label> <div className="card-number-frame border rounded-lg p-3 bg-white" style={{ minHeight: '44px' }} /> </div> <div className="grid grid-cols-2 gap-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Expiry Date </label> <div className="expiry-date-frame border rounded-lg p-3 bg-white" style={{ minHeight: '44px' }} /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> CVV </label> <div className="cvv-frame border rounded-lg p-3 bg-white" style={{ minHeight: '44px' }} /> </div> </div> <button type="button" onClick={handleSubmit} disabled={isSubmitting} className="w-full py-3 px-4 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors" > {isSubmitting ? 'Processing...' : 'Pay Now'} </button> </div> ); }

Webhook Processing

// services/checkout/WebhookService.ts import crypto from 'crypto'; export class CheckoutWebhookService { verifySignature(body: string, signature: string): boolean { const expectedSignature = crypto .createHmac('sha256', CHECKOUT_CONFIG.webhookSecret) .update(body) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) ); } async handleWebhook(event: any): Promise<void> { const { type, data } = event; switch (type) { case 'payment_approved': await this.handlePaymentApproved(data); break; case 'payment_declined': await this.handlePaymentDeclined(data); break; case 'payment_captured': await this.handlePaymentCaptured(data); break; case 'payment_refunded': await this.handlePaymentRefunded(data); break; case 'payment_chargeback': await this.handleChargeback(data); break; } } }

Conclusion

Checkout.com offers enterprise-grade payments with:

  1. Frames.js - PCI compliance without complexity
  2. Flexible 3DS - Control over authentication flows
  3. Transparent pricing - Interchange++ for volume
  4. Split payments - Marketplace-ready

The API is clean, documentation is good, and support is responsive for enterprise accounts.

Related Articles

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 Integrations20 min read

Square Payment Integration for Web Applications

Comprehensive Square Web Payments SDK integration covering card tokenization, Apple Pay, Google Pay, ACH payments, and omnichannel payment strategies for retail and e-commerce.