Payment Integrations

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.

Khalid Aboubakr
24 min read
HyperpayPayment GatewaySaudi ArabiaGccReactNodejsMadaStc Pay

Table of Contents

  1. Why HyperPay Dominates GCC Payments
  2. Integration Architecture
  3. Server-Side Checkout Preparation
  4. Copy and Pay Widget Integration
  5. mada Card Processing
  6. STC Pay Integration
  7. Webhook and Transaction Management
  8. 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:

  1. Data localization - Transaction data should be stored in Saudi Arabia
  2. mada mandatory - Must support mada for domestic transactions
  3. Receipt requirements - Specific fields must be shown on receipts
  4. Refund timelines - Specific timeframes for processing refunds

Conclusion

HyperPay integration requires:

  1. Multiple entity IDs - Different IDs for mada, STC Pay, Apple Pay
  2. BIN detection - Route mada cards correctly
  3. Two-step flow - Prepare checkout → Render widget
  4. 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 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.