HyperPay Payment Integration for Saudi Arabia and GCC
Production-ready HyperPay integration for Saudi and GCC e-commerce. Covers mada card processing, STC Pay, Apple Pay, Copy and Pay forms, and SAMA compliance requirements.
Table of Contents
- Why HyperPay Dominates GCC Payments
- Integration Architecture
- Server-Side Checkout Preparation
- Copy and Pay Widget Integration
- mada Card Processing
- STC Pay Integration
- Webhook and Transaction Management
- SAMA Compliance Considerations
Why HyperPay Dominates GCC Payments
HyperPay is the leading payment gateway in Saudi Arabia and the wider GCC region. After integrating HyperPay for multiple Saudi e-commerce platforms, here's why it matters:
- mada support - The only way to accept Saudi debit cards (70%+ of transactions)
- STC Pay - Saudi's most popular mobile wallet
- SAMA licensed - Full regulatory compliance in Saudi Arabia
- Local acquiring - Direct connections to Saudi banks
If you're building for Saudi Arabia, HyperPay isn't optional—it's mandatory for accepting mada cards.
Integration Architecture
HyperPay uses a two-step process:
┌─────────────────────────────────────────────────────────────────────┐
│ HyperPay Payment Flow │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Prepare Checkout (Server-Side) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Your Server │────────▶│ HyperPay │ │
│ │ │◀────────│ /checkouts │ │
│ └──────────────┘ checkoutId └───────────┘ │
│ │
│ Step 2: Render Payment Form (Client-Side) │
│ ┌──────────────┐ │
│ │ React App │──── checkoutId ────▶ Copy and Pay Widget │
│ └──────────────┘ │
│ │
│ Step 3: Payment Result │
│ ┌──────────────┐◀──── resourcePath ───┐ │
│ │ Your Server │ (redirect) │ HyperPay │
│ │ │ │ │
│ │ GET /payments/{checkoutId} │ │
│ │ to verify result │ │
│ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Server-Side Checkout Preparation
Configuration
// config/hyperpay.ts export const HYPERPAY_CONFIG = { baseUrl: process.env.NODE_ENV === 'production' ? 'https://oppwa.com' : 'https://eu-test.oppwa.com', entityId: process.env.HYPERPAY_ENTITY_ID!, accessToken: process.env.HYPERPAY_ACCESS_TOKEN!, // Separate entity IDs for different payment methods madaEntityId: process.env.HYPERPAY_MADA_ENTITY_ID!, stcPayEntityId: process.env.HYPERPAY_STC_PAY_ENTITY_ID!, applePayEntityId: process.env.HYPERPAY_APPLE_PAY_ENTITY_ID!, currency: 'SAR', };
Checkout Preparation Service
// services/hyperpay/CheckoutService.ts import { HYPERPAY_CONFIG } from '@/config/hyperpay'; interface PrepareCheckoutParams { orderId: string; amount: number; currency: string; paymentType: 'DB' | 'PA'; // DB = Debit, PA = Pre-Authorization customer: { email: string; givenName: string; surname: string; phone?: string; }; billing?: { street1: string; city: string; state?: string; country: string; postcode?: string; }; paymentBrands: string[]; // ['VISA', 'MASTER', 'MADA', 'STC_PAY', 'APPLEPAY'] } export class HyperPayCheckoutService { async prepareCheckout(params: PrepareCheckoutParams): Promise<{ checkoutId: string; entityId: string; }> { const { orderId, amount, currency, paymentType, customer, billing, paymentBrands, } = 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'); } // Determine entity ID based on payment brands const entityId = this.getEntityId(paymentBrands); const requestBody = new URLSearchParams({ entityId, amount: amount.toFixed(2), currency, paymentType, 'customer.email': customer.email, 'customer.givenName': customer.givenName, 'customer.surname': customer.surname, merchantTransactionId: orderId, // Test mode flag (remove in production) ...(process.env.NODE_ENV !== 'production' && { testMode: 'EXTERNAL' }), }); // Add optional fields if (customer.phone) { requestBody.append('customer.phone', customer.phone); } if (billing) { requestBody.append('billing.street1', billing.street1); requestBody.append('billing.city', billing.city); requestBody.append('billing.country', billing.country); if (billing.state) requestBody.append('billing.state', billing.state); if (billing.postcode) requestBody.append('billing.postcode', billing.postcode); } try { const response = await fetch( `${HYPERPAY_CONFIG.baseUrl}/v1/checkouts`, { method: 'POST', headers: { 'Authorization': `Bearer ${HYPERPAY_CONFIG.accessToken}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: requestBody.toString(), } ); const data = await response.json(); if (data.result?.code !== '000.200.100') { console.error('HyperPay checkout preparation failed:', data); throw new PaymentError('CHECKOUT_FAILED', data.result?.description || 'Checkout preparation failed'); } // Store checkout reference await this.paymentRepo.create({ orderId, hyperpayCheckoutId: data.id, amount, currency, status: 'pending', entityId, }); return { checkoutId: data.id, entityId, }; } catch (error) { console.error('HyperPay checkout error:', error); throw error; } } private getEntityId(paymentBrands: string[]): string { // mada requires its own entity ID if (paymentBrands.includes('MADA')) { return HYPERPAY_CONFIG.madaEntityId; } // STC Pay requires its own entity ID if (paymentBrands.includes('STC_PAY')) { return HYPERPAY_CONFIG.stcPayEntityId; } // Apple Pay requires its own entity ID if (paymentBrands.includes('APPLEPAY')) { return HYPERPAY_CONFIG.applePayEntityId; } // Default entity for international cards return HYPERPAY_CONFIG.entityId; } async getPaymentStatus(checkoutId: string): Promise<{ success: boolean; transactionId?: string; paymentBrand?: string; errorMessage?: string; }> { const response = await fetch( `${HYPERPAY_CONFIG.baseUrl}/v1/checkouts/${checkoutId}/payment?entityId=${HYPERPAY_CONFIG.entityId}`, { headers: { 'Authorization': `Bearer ${HYPERPAY_CONFIG.accessToken}`, }, } ); const data = await response.json(); // Success codes start with 000.000 or 000.100 const isSuccess = /^(000\.000\.|000\.100\.)/.test(data.result?.code); return { success: isSuccess, transactionId: data.id, paymentBrand: data.paymentBrand, errorMessage: isSuccess ? undefined : data.result?.description, }; } }
Copy and Pay Widget Integration
React Component
// components/payment/HyperPayCheckout.tsx import { useEffect, useState, useCallback } from 'react'; interface HyperPayCheckoutProps { orderId: string; amount: number; currency: string; customer: { email: string; givenName: string; surname: string; }; onSuccess: (transactionId: string) => void; onError: (error: string) => void; locale?: 'en' | 'ar'; } export function HyperPayCheckout({ orderId, amount, currency, customer, onSuccess, onError, locale = 'en', }: HyperPayCheckoutProps) { const [checkoutId, setCheckoutId] = useState<string | null>(null); const [selectedMethod, setSelectedMethod] = useState<'card' | 'mada' | 'stcpay'>('card'); const [isLoading, setIsLoading] = useState(false); const prepareCheckout = useCallback(async (paymentBrands: string[]) => { setIsLoading(true); try { const response = await fetch('/api/hyperpay/prepare-checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, amount, currency, paymentType: 'DB', customer, paymentBrands, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Checkout preparation failed'); } setCheckoutId(data.checkoutId); } catch (error: any) { onError(error.message); } finally { setIsLoading(false); } }, [orderId, amount, currency, customer, onError]); // Load HyperPay script useEffect(() => { if (!checkoutId) return; const script = document.createElement('script'); script.src = `https://${ process.env.NODE_ENV === 'production' ? 'oppwa.com' : 'eu-test.oppwa.com' }/v1/paymentWidgets.js?checkoutId=${checkoutId}`; script.async = true; document.body.appendChild(script); return () => { document.body.removeChild(script); }; }, [checkoutId]); // Handle payment method selection const handleMethodSelect = (method: 'card' | 'mada' | 'stcpay') => { setSelectedMethod(method); setCheckoutId(null); const brandMap = { card: ['VISA', 'MASTER'], mada: ['MADA'], stcpay: ['STC_PAY'], }; prepareCheckout(brandMap[method]); }; return ( <div className="space-y-6" dir={locale === 'ar' ? 'rtl' : 'ltr'}> {/* Payment Method Selection */} <div className="grid grid-cols-3 gap-4"> <button onClick={() => handleMethodSelect('card')} className={`p-4 border-2 rounded-lg text-center transition-colors ${ selectedMethod === 'card' ? 'border-blue-500 bg-blue-50' : 'border-gray-200' }`} > <div className="text-2xl mb-2">💳</div> <div className="font-medium"> {locale === 'ar' ? 'بطاقة ائتمان' : 'Credit Card'} </div> <div className="text-xs text-gray-500">Visa, Mastercard</div> </button> <button onClick={() => handleMethodSelect('mada')} className={`p-4 border-2 rounded-lg text-center transition-colors ${ selectedMethod === 'mada' ? 'border-green-500 bg-green-50' : 'border-gray-200' }`} > <div className="text-2xl mb-2">🏦</div> <div className="font-medium">mada</div> <div className="text-xs text-gray-500"> {locale === 'ar' ? 'بطاقة مدى' : 'Saudi Debit'} </div> </button> <button onClick={() => handleMethodSelect('stcpay')} className={`p-4 border-2 rounded-lg text-center transition-colors ${ selectedMethod === 'stcpay' ? 'border-purple-500 bg-purple-50' : 'border-gray-200' }`} > <div className="text-2xl mb-2">📱</div> <div className="font-medium">STC Pay</div> <div className="text-xs text-gray-500"> {locale === 'ar' ? 'محفظة STC' : 'STC Wallet'} </div> </button> </div> {/* Loading State */} {isLoading && ( <div className="flex justify-center py-8"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" /> </div> )} {/* Payment Widget */} {checkoutId && !isLoading && ( <form action={`/api/hyperpay/payment-result?orderId=${orderId}`} className="paymentWidgets" data-brands={ selectedMethod === 'card' ? 'VISA MASTER' : selectedMethod === 'mada' ? 'MADA' : 'STC_PAY' } /> )} {/* Amount Display */} <div className="text-center text-lg font-semibold text-gray-700"> {locale === 'ar' ? 'المبلغ:' : 'Total:'} {currency} {amount.toFixed(2)} </div> </div> ); }
mada Card Processing
mada cards require special handling:
Entity ID Configuration
// mada has its own entity ID - NEVER mix with international cards const madaCheckout = await prepareCheckout({ // ... paymentBrands: ['MADA'], // Only MADA, no mixing });
BIN Detection for Card Routing
// utils/cardRouting.ts const MADA_BINS = [ '440647', '440795', '446404', '457865', '458456', '462220', '468540', '468541', '468542', '468543', '484783', '489318', '489319', '493428', '504300', '506968', '508160', '513213', '520058', '521076', '524130', '524514', '529415', '529741', '530060', '530906', '531095', '531196', '532013', '535825', '535989', '536023', '536028', '536030', '539931', '543085', '543357', '549760', '554180', '557606', '558848', '585265', '588845', '588846', '588847', '588848', '588849', '588850', '588851', '588982', '588983', '589005', '589206', '604906', '605141', '636120', '968201', '968202', '968203', '968204', '968205', '968206', '968207', '968208', '968209', '968210', '968211', ]; export function isMadaCard(cardNumber: string): boolean { const bin = cardNumber.replace(/\s/g, '').slice(0, 6); return MADA_BINS.includes(bin); } export function getPaymentBrands(cardNumber: string): string[] { if (isMadaCard(cardNumber)) { return ['MADA']; } return ['VISA', 'MASTER']; }
STC Pay Integration
STC Pay follows a redirect flow:
// STC Pay specific handling async function initiateSTC PayPayment(orderId: string) { const checkout = await prepareCheckout({ orderId, amount, currency: 'SAR', paymentType: 'DB', customer, paymentBrands: ['STC_PAY'], }); // STC Pay will redirect to their app/website // After payment, user is redirected back to your callback URL }
Webhook and Transaction Management
Payment Result Handler
// controllers/HyperPayController.ts router.get('/payment-result', async (req, res) => { const { orderId, resourcePath, id } = req.query; if (!resourcePath) { return res.redirect(`/checkout/failure?orderId=${orderId}`); } try { // Verify payment status const status = await hyperpayService.getPaymentStatus(id as string); if (status.success) { // Update order await orderService.markAsPaid(orderId as string, { transactionId: status.transactionId, paymentMethod: status.paymentBrand, }); return res.redirect(`/checkout/success?orderId=${orderId}`); } else { return res.redirect( `/checkout/failure?orderId=${orderId}&error=${encodeURIComponent(status.errorMessage || 'Payment failed')}` ); } } catch (error) { console.error('Payment verification failed:', error); return res.redirect(`/checkout/failure?orderId=${orderId}`); } });
SAMA Compliance Considerations
Saudi Arabian Monetary Authority (SAMA) has specific requirements:
- Data localization - Transaction data should be stored in Saudi Arabia
- mada mandatory - Must support mada for domestic transactions
- Receipt requirements - Specific fields must be shown on receipts
- Refund timelines - Specific timeframes for processing refunds
Conclusion
HyperPay integration requires:
- Multiple entity IDs - Different IDs for mada, STC Pay, Apple Pay
- BIN detection - Route mada cards correctly
- Two-step flow - Prepare checkout → Render widget
- SAMA compliance - Follow Saudi regulatory requirements
For Saudi e-commerce, HyperPay is essential. Get the mada integration right, and you're serving 70%+ of your customers.
Related Articles
Payment Integrations26 min read
Paymob Payment Integration for Egypt and MENA Markets
Production-ready Paymob Accept API integration for Egypt and MENA markets. Covers iframe tokenization, mobile wallets (Vodafone Cash, Orange Money), webhook security, and Arabic localization patterns.
Payment Integrations25 min read
Amazon Payment Services (PayFort): MENA Integration Guide
Production-ready Amazon Payment Services integration for MENA e-commerce. Covers merchant page integration, tokenization, installments, KNET, and multi-currency processing.
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.