Payment integration is where MENA e-commerce projects consistently get underestimated. Developers coming from a Western background often assume they can use Stripe everywhere, set up a webhook, and ship. The reality in MENA is a fragmented landscape of country-specific gateways, local payment methods that customers actually prefer, regulatory requirements that vary by market, and infrastructure quirks that only become obvious in production. This guide covers every major gateway and payment method across the region, with working Node.js code patterns for the most common integration scenarios.
The MENA Payment Landscape
Understanding why the MENA payment landscape is fragmented starts with banking infrastructure. Saudi Arabia has built a world-class domestic payments network (Mada) that routes debit transactions through a local scheme rather than Visa or Mastercard. Egypt has a dominant fintech network (Fawry) that predates widespread smartphone adoption and serves both banked and unbanked populations. The UAE has high international card penetration but also embraces local methods. Kuwait, Bahrain, and Qatar have affluent, card-comfortable populations where international gateways work without local workarounds.
The result is that a single gateway solution for all of MENA does not exist. The closest candidates are HyperPay (operates in Saudi, UAE, Egypt, Jordan) and Tap Payments (Gulf-focused with Mada support), but even these require country-specific configuration and do not cover Fawry for Egypt or STC Pay for Saudi without add-ons.
Preferred Payment Methods by Country
Before writing a line of integration code, know what your customers actually want to use. Building a checkout around Visa/Mastercard only is a guaranteed conversion killer in Saudi Arabia and Egypt.
Saudi Arabia: Mada (debit), STC Pay (mobile wallet), Tamara/Tabby (BNPL), international cards, Apple Pay. Mada is non-negotiable for any serious Saudi merchant.
UAE: International cards, Apple Pay, Google Pay, Tabby, cash payments at collection points for some demographics.
Egypt: Fawry (cash/wallet), Visa/Mastercard, Meeza (domestic debit), installments via ValU/Sympl, mobile wallets (Vodafone Cash, Etisalat Cash, Orange Money).
Kuwait, Qatar, Bahrain: International cards, Apple Pay, KNET (Kuwait domestic debit), QPay (Qatar), BenefitPay (Bahrain).
Tamara and Tabby — BNPL Integration
Buy Now Pay Later is the single most impactful payment addition for Gulf merchants with basket values above $100. Both Tamara and Tabby offer merchant-friendly SDKs and REST APIs. The integration pattern is nearly identical: create a checkout session on your backend, redirect the customer to the BNPL provider's hosted page, and handle the webhook callback on success or failure.
// src/payments/tamara.ts
import axios from 'axios';
const TAMARA_BASE = 'https://api.tamara.co';
export async function createTamaraSession(order: OrderData): Promise {
const payload = {
order_reference_id: order.id,
total_amount: { amount: order.total.toString(), currency: 'SAR' },
description: `Order #${order.id}`,
country_code: 'SA',
payment_type: 'PAY_BY_INSTALMENTS',
instalments: 3,
locale: 'ar_SA',
items: order.items.map(item => ({
reference_id: item.id,
type: 'Digital',
name: item.name,
sku: item.sku,
quantity: item.quantity,
unit_price: { amount: item.price.toString(), currency: 'SAR' },
total_amount: { amount: (item.price * item.quantity).toString(), currency: 'SAR' },
})),
consumer: {
first_name: order.customer.firstName,
last_name: order.customer.lastName,
phone_number: order.customer.phone,
email: order.customer.email,
},
billing_address: order.billingAddress,
shipping_address: order.shippingAddress,
merchant_url: {
success: `${process.env.BASE_URL}/checkout/success`,
failure: `${process.env.BASE_URL}/checkout/failure`,
cancel: `${process.env.BASE_URL}/checkout/cancel`,
notification: `${process.env.BASE_URL}/webhooks/tamara`,
},
};
const { data } = await axios.post(`${TAMARA_BASE}/checkout`, payload, {
headers: { Authorization: `Bearer ${process.env.TAMARA_API_TOKEN}` },
});
return data.checkout_url;
}
Mada Integration (via Tap Payments)
Mada is a domestic debit scheme — you cannot integrate with it directly. You access it through an acquirer like Tap Payments, Moyasar, or HyperPay. The developer experience with Tap Payments is the cleanest for Mada. The key technical requirement is that Mada transactions must be 3DS-authenticated, meaning your checkout flow must handle the 3DS redirect correctly.
// src/payments/tap.ts
import axios from 'axios';
const TAP_BASE = 'https://api.tap.company/v2';
export async function createTapCharge(order: OrderData): Promise {
const payload = {
amount: order.total,
currency: 'SAR',
threeDSecure: true,
save_card: false,
description: `Order #${order.id}`,
statement_descriptor: 'Store Name',
metadata: { orderId: order.id },
reference: { transaction: order.id, order: order.id },
receipt: { email: true, sms: false },
customer: {
first_name: order.customer.firstName,
last_name: order.customer.lastName,
email: order.customer.email,
phone: { country_code: '966', number: order.customer.phone },
},
source: { id: 'src_all' }, // accepts Mada, Visa, Mastercard, Apple Pay
post: { url: `${process.env.BASE_URL}/webhooks/tap` },
redirect: { url: `${process.env.BASE_URL}/checkout/tap-return` },
};
const { data } = await axios.post(`${TAP_BASE}/charges`, payload, {
headers: {
Authorization: `Bearer ${process.env.TAP_SECRET_KEY}`,
'Content-Type': 'application/json',
},
});
return data;
}
PayMob — Egypt's Developer Gateway
PayMob is the most developer-friendly payment gateway in Egypt. It handles cards, Fawry, mobile wallets (Vodafone Cash, Etisalat Cash, Orange Money), ValU installments, and Meeza. The integration flow has three steps: authenticate to get a token, create an order, then create a payment key.
// src/payments/paymob.ts
import axios from 'axios';
const PAYMOB_BASE = 'https://accept.paymob.com/api';
async function getAuthToken(): Promise {
const { data } = await axios.post(`${PAYMOB_BASE}/auth/tokens`, {
api_key: process.env.PAYMOB_API_KEY,
});
return data.token;
}
async function createPaymobOrder(token: string, amount: number): Promise {
const { data } = await axios.post(
`${PAYMOB_BASE}/ecommerce/orders`,
{
auth_token: token,
delivery_needed: false,
amount_cents: Math.round(amount * 100),
currency: 'EGP',
items: [],
}
);
return data.id;
}
export async function createPaymobPaymentKey(
order: OrderData,
integrationId: number
): Promise {
const token = await getAuthToken();
const orderId = await createPaymobOrder(token, order.total);
const { data } = await axios.post(`${PAYMOB_BASE}/acceptance/payment_keys`, {
auth_token: token,
amount_cents: Math.round(order.total * 100),
expiration: 3600,
order_id: orderId,
billing_data: {
first_name: order.customer.firstName,
last_name: order.customer.lastName,
email: order.customer.email,
phone_number: order.customer.phone,
country: 'EG',
city: 'Cairo',
street: 'NA',
building: 'NA',
floor: 'NA',
apartment: 'NA',
},
currency: 'EGP',
integration_id: integrationId,
});
return data.token;
}
Moyasar — Saudi-First Gateway
Moyasar is built specifically for Saudi Arabia and provides excellent Mada support, Apple Pay on the web, and a clean REST API. The publishable key is used client-side in their JavaScript SDK; the secret key is for server-side calls. Moyasar's hosted checkout (mysr.app) handles PCI compliance automatically.
// src/payments/moyasar.ts
import axios from 'axios';
export async function createMoyasarPayment(order: OrderData): Promise {
const { data } = await axios.post(
'https://api.moyasar.com/v1/payments',
{
amount: Math.round(order.total * 100), // in halalas
currency: 'SAR',
description: `Order #${order.id}`,
source: {
type: 'creditcard',
name: order.customer.fullName,
number: order.cardNumber,
cvc: order.cardCvc,
month: order.cardMonth,
year: order.cardYear,
},
callback_url: `${process.env.BASE_URL}/webhooks/moyasar`,
},
{
auth: { username: process.env.MOYASAR_SECRET_KEY!, password: '' },
}
);
return data;
}
Webhooks and Idempotency
Every MENA gateway delivers payment results via webhooks. The critical rule: verify the webhook signature before processing, respond with 200 OK immediately, and process asynchronously. Most importantly, implement idempotency — the same webhook event can be delivered multiple times. Use the payment ID as an idempotency key to ensure you only fulfill an order once.
// src/webhooks/payment-handler.ts
import { Request, Response } from 'express';
import crypto from 'crypto';
import { db } from '../db';
import { fulfillOrder } from '../orders';
export async function handlePaymentWebhook(req: Request, res: Response) {
// Respond immediately — always
res.status(200).send('OK');
const event = req.body;
const paymentId = event.id ?? event.payment_id ?? event.obj?.id;
if (!paymentId) return;
// Idempotency check
const existing = await db.processedPayments.findUnique({ where: { paymentId } });
if (existing) return; // already processed
// Mark as being processed
await db.processedPayments.create({ data: { paymentId, processedAt: new Date() } });
// Fulfill based on status
if (event.status === 'paid' || event.status === 'CAPTURED' || event.status === 'approved') {
const orderId = event.metadata?.orderId ?? event.order_reference_id;
await fulfillOrder(orderId);
}
}
PCI DSS Basics for MENA Developers
PCI DSS compliance is a legal requirement for any system that handles cardholder data. The simplest path: use a gateway's hosted page or JavaScript SDK so that raw card data never touches your server. If you use a hosted checkout (Tap, Moyasar, PayMob's hosted form), you are effectively in PCI SAQ A — the most minimal compliance level, requiring only a self-assessment questionnaire. Never proxy card numbers through your own server unless you intend to pursue full PCI DSS Level 1 certification — a multi-month, expensive process.
Multi-Currency and Refunds
Most MENA gateways settle in local currency. If you are building a multi-country merchant platform, you need to handle currency conversion before passing amounts to each gateway. Store amounts as integers (cents or halalas) to avoid floating-point rounding errors — a classic payment bug that costs merchants real money.
// src/payments/currency.ts
// Store all amounts as integers (smallest currency unit)
export function toSmallestUnit(amount: number, currency: string): number {
const zeroDecimal = ['BHD', 'KWD', 'OMR']; // 3 decimal places
if (zeroDecimal.includes(currency)) return Math.round(amount * 1000);
return Math.round(amount * 100);
}
export function fromSmallestUnit(amount: number, currency: string): number {
const zeroDecimal = ['BHD', 'KWD', 'OMR'];
if (zeroDecimal.includes(currency)) return amount / 1000;
return amount / 100;
}
Testing Sandboxes
Every major MENA gateway provides a sandbox environment with test card numbers. Always write integration tests that cover the full payment lifecycle: success, failure, and webhook delivery. For Mada testing with Tap, use their published Mada test card numbers. For PayMob, the test environment mirrors production behaviour including Fawry code generation (the test Fawry code can be paid at any test endpoint).
Which Gateway to Choose
Decision tree: Saudi-only merchant needing Mada support → Moyasar or Tap Payments. Egypt-only merchant → PayMob. Gulf merchant wanting BNPL → add Tamara (Saudi-primary) or Tabby (Gulf-wide). Multi-country Gulf operation → HyperPay with Tap as a Mada-specific fallback. International merchant wanting the simplest integration → Stripe where available, Tap or HyperPay where Stripe does not support Mada. Always check each gateway's settlement speed, transaction fees (typically 2–3.5% + fixed fee), and dispute handling process before committing.