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.
Table of Contents
- Why Paymob for MENA Markets
- Understanding the Accept API Flow
- Server-Side Implementation
- React Integration with Accept.js
- Mobile Wallet Integration
- Webhook Security and Processing
- Handling Egyptian Market Specifics
- Common Integration Pitfalls
Why Paymob for MENA Markets
Paymob dominates the Egyptian payment landscape and is expanding across MENA. Having integrated Paymob for multiple Egyptian e-commerce platforms, I can tell you why it matters:
- Local payment methods - Vodafone Cash, Orange Money, Fawry, Meeza cards
- Egyptian acquiring - Direct local processing, no international fees
- Arabic-first experience - Native RTL support and Arabic UI
- Regulatory compliance - CBE (Central Bank of Egypt) licensed
If you're building for Egypt or expanding into MENA, Paymob is often the only realistic choice for accepting local payment methods.
Understanding the Accept API Flow
Paymob's Accept API uses a multi-step authentication flow that differs from Western gateways:
┌─────────────────────────────────────────────────────────────────────┐
│ Payment Flow Architecture │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Authentication Token │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Your Server │────────▶│ Paymob API │ │
│ │ │◀────────│ /auth/tokens│ │
│ └──────────────┘ token └──────────────┘ │
│ │
│ Step 2: Order Registration │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Your Server │────────▶│ /orders │ │
│ │ │◀────────│ │ │
│ └──────────────┘ order_id└──────────────┘ │
│ │
│ Step 3: Payment Key Generation │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Your Server │────────▶│ /payment_keys│ │
│ │ │◀────────│ │ │
│ └──────────────┘ key └──────────────┘ │
│ │
│ Step 4: Client-Side Payment │
│ ┌──────────────┐ │
│ │ React App │──── payment_key ────▶ Paymob iframe │
│ └──────────────┘ │
│ │
│ Step 5: Webhook Notification │
│ ┌──────────────┐◀──── Transaction ────┐ │
│ │ Your Server │ Result │ Paymob │
│ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Critical insight: Unlike Stripe where you create a PaymentIntent and you're done, Paymob requires three sequential API calls before the client can pay. Cache your auth token—it's valid for 1 hour.
Server-Side Implementation
Configuration
// config/paymob.ts export const PAYMOB_CONFIG = { apiKey: process.env.PAYMOB_API_KEY!, integrationId: process.env.PAYMOB_INTEGRATION_ID!, // Card payments walletIntegrationId: process.env.PAYMOB_WALLET_INTEGRATION_ID!, // Mobile wallets iframeId: process.env.PAYMOB_IFRAME_ID!, hmacSecret: process.env.PAYMOB_HMAC_SECRET!, baseUrl: 'https://accept.paymob.com/api', }; // Token cache - auth tokens are valid for 1 hour let cachedToken: { token: string; expiresAt: number } | null = null; export async function getAuthToken(): Promise<string> { // Return cached token if still valid (with 5-minute buffer) if (cachedToken && cachedToken.expiresAt > Date.now() + 5 * 60 * 1000) { return cachedToken.token; } const response = await fetch(`${PAYMOB_CONFIG.baseUrl}/auth/tokens`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_key: PAYMOB_CONFIG.apiKey }), }); if (!response.ok) { throw new Error('Paymob authentication failed'); } const data = await response.json(); cachedToken = { token: data.token, expiresAt: Date.now() + 55 * 60 * 1000, // 55 minutes }; return data.token; }
Complete Payment Service
// services/paymob/PaymentService.ts import { PAYMOB_CONFIG, getAuthToken } from '@/config/paymob'; interface PaymobOrderParams { orderId: string; amountCents: number; currency: string; customer: { firstName: string; lastName: string; email: string; phone: string; }; billingData: { apartment?: string; floor?: string; street?: string; building?: string; city: string; state?: string; country: string; postalCode?: string; }; items?: Array<{ name: string; amount_cents: number; quantity: number; }>; } export class PaymobPaymentService { async createPaymentKey(params: PaymobOrderParams): Promise<{ paymentKey: string; orderId: number; iframeUrl: string; }> { const { orderId, amountCents, currency, customer, billingData, items, } = params; // Validate order const order = await this.orderRepo.findById(orderId); if (!order || order.totalAmountCents !== amountCents) { throw new PaymentError('INVALID_ORDER', 'Order validation failed'); } try { // Step 1: Get auth token const authToken = await getAuthToken(); // Step 2: Register order with Paymob const orderResponse = await fetch( `${PAYMOB_CONFIG.baseUrl}/ecommerce/orders`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ auth_token: authToken, delivery_needed: false, amount_cents: amountCents, currency: currency.toUpperCase(), merchant_order_id: orderId, items: items || [ { name: `Order #${order.orderNumber}`, amount_cents: amountCents, quantity: 1, }, ], }), } ); if (!orderResponse.ok) { const error = await orderResponse.json(); console.error('Paymob order registration failed:', error); throw new PaymentError('ORDER_REGISTRATION_FAILED', 'Could not register order'); } const orderData = await orderResponse.json(); const paymobOrderId = orderData.id; // Step 3: Generate payment key const paymentKeyResponse = await fetch( `${PAYMOB_CONFIG.baseUrl}/acceptance/payment_keys`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ auth_token: authToken, amount_cents: amountCents, expiration: 3600, // 1 hour order_id: paymobOrderId, billing_data: { apartment: billingData.apartment || 'NA', email: customer.email, floor: billingData.floor || 'NA', first_name: customer.firstName, street: billingData.street || 'NA', building: billingData.building || 'NA', phone_number: customer.phone, shipping_method: 'NA', postal_code: billingData.postalCode || 'NA', city: billingData.city, country: billingData.country, last_name: customer.lastName, state: billingData.state || 'NA', }, currency: currency.toUpperCase(), integration_id: PAYMOB_CONFIG.integrationId, lock_order_when_paid: true, }), } ); if (!paymentKeyResponse.ok) { const error = await paymentKeyResponse.json(); console.error('Paymob payment key generation failed:', error); throw new PaymentError('PAYMENT_KEY_FAILED', 'Could not generate payment key'); } const paymentKeyData = await paymentKeyResponse.json(); // Store payment record await this.paymentRepo.create({ orderId, paymobOrderId, paymentKey: paymentKeyData.token, amountCents, currency, status: 'pending', }); return { paymentKey: paymentKeyData.token, orderId: paymobOrderId, iframeUrl: `https://accept.paymob.com/api/acceptance/iframes/${PAYMOB_CONFIG.iframeId}?payment_token=${paymentKeyData.token}`, }; } catch (error) { console.error('Paymob payment creation failed:', error); throw error; } } // For mobile wallet payments (Vodafone Cash, Orange Money) async createWalletPayment( params: PaymobOrderParams & { walletPhone: string } ): Promise<{ redirectUrl: string; paymobOrderId: number }> { const { walletPhone, ...orderParams } = params; // Get payment key with wallet integration ID const authToken = await getAuthToken(); // Register order (same as card flow) const orderResponse = await fetch( `${PAYMOB_CONFIG.baseUrl}/ecommerce/orders`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auth_token: authToken, delivery_needed: false, amount_cents: orderParams.amountCents, currency: orderParams.currency, merchant_order_id: orderParams.orderId, }), } ); const orderData = await orderResponse.json(); // Generate payment key for wallet const paymentKeyResponse = await fetch( `${PAYMOB_CONFIG.baseUrl}/acceptance/payment_keys`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auth_token: authToken, amount_cents: orderParams.amountCents, expiration: 3600, order_id: orderData.id, billing_data: { email: orderParams.customer.email, first_name: orderParams.customer.firstName, last_name: orderParams.customer.lastName, phone_number: orderParams.customer.phone, // Required fields with NA fallback apartment: 'NA', floor: 'NA', street: 'NA', building: 'NA', city: orderParams.billingData.city, country: orderParams.billingData.country, state: 'NA', postal_code: 'NA', shipping_method: 'NA', }, currency: orderParams.currency, integration_id: PAYMOB_CONFIG.walletIntegrationId, }), } ); const paymentKeyData = await paymentKeyResponse.json(); // Initiate wallet payment const walletResponse = await fetch( `${PAYMOB_CONFIG.baseUrl}/acceptance/payments/pay`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source: { identifier: walletPhone, subtype: 'WALLET', }, payment_token: paymentKeyData.token, }), } ); const walletData = await walletResponse.json(); return { redirectUrl: walletData.redirect_url, paymobOrderId: orderData.id, }; } }
React Integration with Accept.js
Payment Form Component
// components/payment/PaymobPayment.tsx import { useState, useCallback } from 'react'; interface PaymobPaymentProps { orderId: string; amount: number; currency: string; customer: { firstName: string; lastName: string; email: string; phone: string; }; onSuccess: (transactionId: string) => void; onError: (error: string) => void; } export function PaymobPayment({ orderId, amount, currency, customer, onSuccess, onError, }: PaymobPaymentProps) { const [isLoading, setIsLoading] = useState(false); const [iframeUrl, setIframeUrl] = useState<string | null>(null); const [paymentMethod, setPaymentMethod] = useState<'card' | 'wallet'>('card'); const [walletPhone, setWalletPhone] = useState(''); const initiateCardPayment = useCallback(async () => { setIsLoading(true); try { const response = await fetch('/api/paymob/create-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, amountCents: Math.round(amount * 100), currency, customer, billingData: { city: 'Cairo', country: 'EG', }, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Payment initialization failed'); } setIframeUrl(data.iframeUrl); } catch (error: any) { onError(error.message); } finally { setIsLoading(false); } }, [orderId, amount, currency, customer, onError]); const initiateWalletPayment = useCallback(async () => { if (!walletPhone || walletPhone.length < 11) { onError('Please enter a valid wallet phone number'); return; } setIsLoading(true); try { const response = await fetch('/api/paymob/create-wallet-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, amountCents: Math.round(amount * 100), currency, customer, walletPhone: walletPhone.startsWith('+2') ? walletPhone : `+2${walletPhone}`, billingData: { city: 'Cairo', country: 'EG', }, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Wallet payment failed'); } // Redirect to wallet provider window.location.href = data.redirectUrl; } catch (error: any) { onError(error.message); } finally { setIsLoading(false); } }, [orderId, amount, currency, customer, walletPhone, onError]); // Listen for iframe messages useEffect(() => { const handleMessage = (event: MessageEvent) => { // Verify origin if (!event.origin.includes('paymob.com')) return; const data = event.data; if (data.success) { onSuccess(data.transaction_id); } else { onError(data.message || 'Payment failed'); } }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, [onSuccess, onError]); if (iframeUrl) { return ( <div className="relative"> <iframe src={iframeUrl} className="w-full h-[500px] border-0 rounded-lg" title="Paymob Payment" allow="payment" /> <button onClick={() => setIframeUrl(null)} className="mt-4 text-sm text-gray-500 hover:text-gray-700" > ← Choose different payment method </button> </div> ); } return ( <div className="space-y-6"> {/* Payment Method Selection */} <div className="flex gap-4"> <button onClick={() => setPaymentMethod('card')} className={`flex-1 p-4 border-2 rounded-lg transition-colors ${ paymentMethod === 'card' ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300' }`} > <div className="text-lg font-medium">💳 Card Payment</div> <div className="text-sm text-gray-500">Visa, Mastercard, Meeza</div> </button> <button onClick={() => setPaymentMethod('wallet')} className={`flex-1 p-4 border-2 rounded-lg transition-colors ${ paymentMethod === 'wallet' ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300' }`} > <div className="text-lg font-medium">📱 Mobile Wallet</div> <div className="text-sm text-gray-500">Vodafone Cash, Orange</div> </button> </div> {/* Card Payment */} {paymentMethod === 'card' && ( <button onClick={initiateCardPayment} disabled={isLoading} className="w-full py-3 px-4 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition-colors" > {isLoading ? 'Processing...' : `Pay ${currency} ${amount.toFixed(2)}`} </button> )} {/* Wallet Payment */} {paymentMethod === 'wallet' && ( <div className="space-y-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Wallet Phone Number </label> <input type="tel" value={walletPhone} onChange={(e) => setWalletPhone(e.target.value)} placeholder="01xxxxxxxxx" className="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" dir="ltr" /> <p className="mt-1 text-sm text-gray-500"> Enter your Vodafone Cash or Orange Money number </p> </div> <button onClick={initiateWalletPayment} disabled={isLoading || !walletPhone} className="w-full py-3 px-4 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 disabled:bg-gray-400 transition-colors" > {isLoading ? 'Processing...' : 'Pay with Mobile Wallet'} </button> </div> )} </div> ); }
Webhook Security and Processing
HMAC Verification
// middleware/paymobWebhook.ts import crypto from 'crypto'; import { PAYMOB_CONFIG } from '@/config/paymob'; export function verifyPaymobHmac(data: any, receivedHmac: string): boolean { // Paymob HMAC is calculated from specific fields in order const hmacString = [ data.amount_cents, data.created_at, data.currency, data.error_occured, data.has_parent_transaction, data.id, data.integration_id, data.is_3d_secure, data.is_auth, data.is_capture, data.is_refunded, data.is_standalone_payment, data.is_voided, data.order?.id || data.order, data.owner, data.pending, data.source_data?.pan || '', data.source_data?.sub_type || '', data.source_data?.type || '', data.success, ].join(''); const calculatedHmac = crypto .createHmac('sha512', PAYMOB_CONFIG.hmacSecret) .update(hmacString) .digest('hex'); return calculatedHmac === receivedHmac; }
Webhook Handler
// services/paymob/WebhookService.ts export class PaymobWebhookService { async handleCallback(data: any, hmac: string): Promise<void> { // Verify HMAC if (!verifyPaymobHmac(data, hmac)) { throw new Error('Invalid HMAC signature'); } const transactionId = data.id; const orderId = data.order?.merchant_order_id || data.merchant_order_id; const success = data.success === true || data.success === 'true'; // Idempotency check const existing = await this.webhookRepo.findByTransactionId(transactionId); if (existing) { console.log(`Transaction ${transactionId} already processed`); return; } await this.webhookRepo.create({ transactionId, orderId, success, data, }); if (success) { await this.handleSuccessfulPayment(orderId, transactionId, data); } else { await this.handleFailedPayment(orderId, transactionId, data); } } private async handleSuccessfulPayment( orderId: string, transactionId: string, data: any ): Promise<void> { await this.paymentRepo.updateByOrderId(orderId, { status: 'paid', transactionId, paidAt: new Date(), paymentMethod: data.source_data?.type, cardLastFour: data.source_data?.pan, }); await this.orderService.fulfillOrder(orderId); await this.notificationService.sendOrderConfirmation(orderId); } private async handleFailedPayment( orderId: string, transactionId: string, data: any ): Promise<void> { const errorMessage = this.mapPaymobError(data.data?.message || data.txn_response_code); await this.paymentRepo.updateByOrderId(orderId, { status: 'failed', transactionId, failureReason: errorMessage, }); await this.notificationService.sendPaymentFailed(orderId, errorMessage); } private mapPaymobError(code: string): string { const errorMap: Record<string, string> = { 'DECLINED': 'Your card was declined. Please try a different card.', 'INSUFFICIENT_FUNDS': 'Insufficient funds. Please try a different card.', 'EXPIRED_CARD': 'Your card has expired.', 'INVALID_CARD': 'Invalid card details. Please check and try again.', 'DO_NOT_HONOR': 'Transaction not approved. Please contact your bank.', }; return errorMap[code] || 'Payment failed. Please try again.'; } }
Handling Egyptian Market Specifics
Currency and Amount Handling
// utils/paymobCurrency.ts // Egyptian Pound (EGP) uses piasters (1 EGP = 100 piasters) export function toPaymobAmount(amount: number, currency: string): number { // Paymob always expects amount in cents/piasters return Math.round(amount * 100); } export function fromPaymobAmount(amountCents: number, currency: string): number { return amountCents / 100; } // Format for display with Arabic numerals option export function formatEgyptianPrice( amount: number, useArabicNumerals: boolean = false ): string { const formatted = new Intl.NumberFormat('ar-EG', { style: 'currency', currency: 'EGP', }).format(amount); if (!useArabicNumerals) { // Convert Arabic numerals to Western return formatted.replace(/[٠-٩]/g, (d) => '0123456789'['٠١٢٣٤٥٦٧٨٩'.indexOf(d)] ); } return formatted; }
Phone Number Validation
// Egyptian mobile numbers: 01x xxxx xxxx (11 digits) export function validateEgyptianPhone(phone: string): boolean { const cleaned = phone.replace(/\D/g, ''); // Remove country code if present const normalized = cleaned.startsWith('20') ? cleaned.slice(2) : cleaned.startsWith('002') ? cleaned.slice(3) : cleaned; // Must be 11 digits starting with 01 return /^01[0125][0-9]{8}$/.test(normalized); } // Carrier detection for wallet payments export function detectCarrier(phone: string): 'vodafone' | 'orange' | 'etisalat' | 'we' | null { const normalized = phone.replace(/\D/g, '').slice(-10); const prefix = normalized.slice(0, 3); const carriers: Record<string, 'vodafone' | 'orange' | 'etisalat' | 'we'> = { '010': 'vodafone', '011': 'etisalat', '012': 'orange', '015': 'we', }; return carriers[prefix] || null; }
Common Integration Pitfalls
1. Auth Token Expiration
// ❌ BAD: Getting new token for every request async function createPayment() { const token = await getNewAuthToken(); // Wasteful! // ... } // ✅ GOOD: Cache and reuse tokens async function createPayment() { const token = await getAuthToken(); // Uses cache // ... }
2. Missing Billing Data Fields
// ❌ BAD: Paymob will reject this const billingData = { email: customer.email, phone_number: customer.phone, }; // ✅ GOOD: All fields required (use 'NA' for missing) const billingData = { apartment: customer.apartment || 'NA', email: customer.email, floor: customer.floor || 'NA', first_name: customer.firstName, street: customer.street || 'NA', building: customer.building || 'NA', phone_number: customer.phone, shipping_method: 'NA', postal_code: customer.postalCode || 'NA', city: customer.city, country: customer.country, last_name: customer.lastName, state: customer.state || 'NA', };
3. HMAC Field Order
The HMAC calculation is order-sensitive. Always concatenate fields in the exact order Paymob documents.
Conclusion
Paymob integration requires understanding:
- Multi-step flow - Auth → Order → Payment Key → Payment
- Token caching - Reuse auth tokens (1-hour validity)
- Mobile wallets - Different integration ID and flow
- HMAC security - Field order matters
- Egyptian specifics - Phone formats, currency handling
For Egyptian e-commerce, Paymob is essential. Master these patterns and you'll have a reliable payment foundation.
Related Articles
Payment Integrations24 min read
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.
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.
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.