If you are building a custom store for Saudi Arabia or the Gulf, the payment stack is not a plugin decision — it is an architecture decision, and getting it wrong costs you the launch date. Here is the real integration path for Tabby, Tamara, mada and PayTabs: what each provider demands before you write a line of code, how the webhooks actually behave under load, and what happens to your money after the customer taps pay.
I have shipped Gulf checkouts on Laravel, on Node, and on top of other people's half-finished code. The pattern is always the same. The founder assumes payments are the easy part because a plugin exists. Then onboarding takes three weeks, the sandbox behaves nothing like production, a webhook fires twice, and an order gets fulfilled that was never captured.
None of that is in the provider documentation. It is in this article.
1. The verdict, before the detail
If you only read one section, read this one.
Launch with one card gateway, not three. Pick a SAMA-licensed processor that gives you mada, Visa/Mastercard and Apple Pay through a single integration — PayTabs, HyperPay, Moyasar or Tap in Saudi Arabia; PayTabs, Telr or Checkout.com in the UAE; Paymob if Egypt is in scope. One integration, one settlement report, one reconciliation job.
Add BNPL second, not first. Tabby and Tamara are conversion tools, not payment infrastructure. They are worth adding — Gulf merchants consistently report higher average order values after BNPL goes live — but they are a separate contract, a separate onboarding, a separate webhook contract and a separate refund flow. Do not let them block your launch.
You cannot use Stripe as a Saudi entity. Not a preference, a fact. More on that in section 7.
mada is not optional. It is the dominant card in the Kingdom by a very wide margin. A Saudi checkout without mada is a Saudi checkout that loses most of its customers at the last step.
Never trust a webhook payload as the source of truth. Treat every incoming notification as a hint that something changed, then call the provider's retrieve endpoint and believe that instead. This single rule prevents most of the money-losing bugs I get called in to fix.
2. What Gulf customers actually pay with
Published market-share numbers for Saudi checkout vary a lot between sources, so treat any precise percentage you read — including the ones vendors quote at you — as directional rather than measured. What is not in dispute is the ordering.
mada dominates. It is the domestic debit scheme created by the Saudi Central Bank, it sits on almost every Saudi bank card, and Saudi consumers reach for debit first. International Visa and Mastercard credit come second, largely from expatriate and corporate customers. Apple Pay has grown fast enough that it is now a first-class requirement rather than a nice extra. STC Pay holds a meaningful wallet share. BNPL through Tabby and Tamara is a smaller slice of transaction count but a disproportionate slice of basket value. Cash on delivery is shrinking but has not disappeared, especially outside the big cities.
The design implication: your checkout needs mada and Apple Pay on day one, card as the fallback, BNPL as the upsell, and — if your category justifies it — cash on delivery with a reconciliation process that is not a spreadsheet. If you are still at the stage of choosing your overall stack and store architecture, my ecommerce development guide covers the layers underneath this one.
The thing nobody tells you about mada online: mada cards are debit. Debit means the customer's actual bank balance, an OTP on every transaction, and a hard 3D Secure step that you cannot skip. Your checkout must survive a full redirect out to the bank's authentication page and back, on a phone, on a patchy mobile connection, sometimes with the app switching to the banking app for the OTP. If your order state depends on the customer returning to your success URL, you will lose orders. Design for the server-to-server notification as the primary signal and the browser redirect as a convenience.
3. How do I add Tabby and Tamara to a custom-built store?
This is the question I get most, and the honest answer has two halves: the commercial half takes longer than the technical half.
Before any code: the commercial track
Both Tabby and Tamara require you to be an approved merchant before you get production keys. That means a commercial registration in the market you are selling into, a bank account in that market, business details, category review, and in most cases a signed merchant agreement with a negotiated rate. Some categories get declined or priced punitively — high-refund verticals, digital goods, anything that looks like resale of stored value.
You can usually get sandbox credentials quickly to start building. You cannot go live without the contract. Start the commercial track on day one of the project, in parallel with development, or it becomes your critical path.
The Tabby flow
Tabby's model is a checkout session followed by a payment object. The lifecycle is worth learning properly because most integration bugs come from misunderstanding it.
- Create a checkout session server-side with the order amount, currency, buyer details, order items, shipping address and — this matters — buyer history and order history.
- Tabby pre-scores the customer. The response tells you whether the customer is eligible and which installment products are available. If Tabby returns a rejected status, no product is available for that buyer and you must hide or disable the Tabby option gracefully rather than letting them click into a dead end.
- Redirect the customer to the returned Tabby web URL, where they authenticate and approve the plan.
- The payment becomes authorized. Money is committed but not yet yours.
- You capture — fully or partially — when you ship. Full capture closes the payment. That is the final state.
- You refund against a captured payment, or void an authorization you never captured.
The buyer-history payload is the part developers skip and then wonder why approval rates are poor. Tabby uses registration date, previous order count and previous order values as scoring input. If you send an empty history for a five-year customer with forty completed orders, you are throwing away the strongest signal you have. Populate it from your own orders table. It costs you one query.
// Laravel: create a Tabby checkout session
$payload = [
'payment' => [
'amount' => number_format($order->total, 2, '.', ''),
'currency' => 'SAR',
'description' => "Order #{$order->reference}",
'buyer' => [
'phone' => $customer->phone_e164,
'email' => $customer->email,
'name' => $customer->full_name,
],
'buyer_history' => [
'registered_since' => $customer->created_at->toIso8601String(),
'loyalty_level' => $customer->completed_orders_count,
],
'order' => [
'reference_id' => $order->reference,
'items' => $this->mapItems($order),
],
'order_history' => $this->lastOrders($customer, 10),
'shipping_address' => [
'city' => $order->city,
'address' => $order->address_line,
],
],
'lang' => app()->getLocale(),
'merchant_code' => config('tabby.merchant_code'),
'merchant_urls' => [
'success' => route('checkout.tabby.success', $order),
'cancel' => route('checkout.tabby.cancel', $order),
'failure' => route('checkout.tabby.failure', $order),
],
];
$response = Http::withToken(config('tabby.public_key'))
->timeout(15)
->post(config('tabby.base_url') . '/api/v2/checkout', $payload)
->throw()
->json();
if (($response['status'] ?? null) === 'rejected') {
// No product available for this buyer. Hide Tabby, do not redirect.
return back()->with('bnpl_unavailable', true);
}
Note the regional base URL. Tabby serves the Kingdom and the other Gulf markets from different domains with identical paths, and the environment (test versus live) is determined by which key you authenticate with, not by a flag in the request. Configure the base URL per market and never hardcode it.
The Tamara flow
Tamara is structurally similar but with meaningfully different semantics that will bite you if you assume symmetry.
- Create a checkout session with purchase details, consumer data, itemised cart, and your success, failure, cancel and notification URLs.
- Customer approves at Tamara's hosted checkout. The order moves to approved.
- You must explicitly authorise. This is the step people miss. An approved Tamara order is not a completed one — you call the Authorise Order API to confirm you received and accepted it. Skip this and the order never becomes collectable.
- You capture on shipment.
- Tamara auto-captures if you do not. Their documented behaviour is that an authorised order left uncaptured for twenty-one days gets auto-captured and moved to fully captured status.
That auto-capture window is the single most consequential difference between the two providers for anyone selling made-to-order goods, pre-orders, or anything with a long fulfilment tail. If your production lead time is thirty days, Tamara will capture on day twenty-one whether you shipped or not, your ledger will say captured, your warehouse will say nothing has left, and your accountant will find the gap next quarter. Build a scheduled job that flags authorised-but-unshipped orders well before the window closes and forces a human decision.
Tamara's webhook authentication also differs. You register the notification endpoint in their partner portal, choose which order-status events you want, and Tamara signs deliveries with a JWT notification token — passed both as a query parameter and as a bearer token in the authorization header — which you decode with your notification secret to confirm the payload was not tampered with in transit.
Presenting BNPL correctly on the product page
Both providers give you promotional snippets or SDK widgets for the "4 payments of X" messaging on product and cart pages. Use them, but render the amount server-side or with a client-side calculation you control, and make sure the widget cannot block first paint. I have seen a third-party BNPL badge script add most of a second to largest contentful paint on a product page — which costs you more conversions than the badge wins. If page speed is already a concern for you, the reasons your site loads slowly usually start with exactly this kind of third-party script.
4. Does mada require a local Saudi merchant account?
Yes, effectively — and this is the requirement that most often surprises founders operating from outside the Kingdom.
mada is a domestic scheme governed by the Saudi Central Bank. To accept mada online you need a merchant ID issued through a Saudi acquirer, which in practice means contracting with a SAMA-licensed payment gateway or acquiring bank. The gateway holds the licence; you do not need your own SAMA licence as a merchant. But you do need to be onboardable by a licensed local provider, and their onboarding will ask for:
- A Saudi commercial registration, or in some cases a freelance certificate for individual practitioners.
- A Saudi bank account in the registered entity's name for settlement.
- Owner or authorised-signatory identity documents.
- Your website live, with visible pricing, terms, refund policy, contact details and — increasingly — Arabic content.
- Category and volume declarations, sometimes a sample of the checkout flow.
A foreign company with no Saudi entity has two realistic routes: register a Saudi entity, or work with an international acquirer that holds Saudi acquiring capability and can present mada to your merchant account. The second route exists but is generally reserved for larger volumes.
One more practical detail: mada cards issued today are co-badged. Domestically they route over the mada rails; internationally, the Visa or Mastercard side handles it. Your gateway's routing decision affects your interchange cost. If your volumes are material, ask your provider directly how domestic mada transactions are routed and priced versus the co-badged scheme rails — the difference is real money at scale, and it is a question most merchants never ask.
Confirm the exact document list and the current fee schedule with the provider before you commit. Onboarding requirements and pricing in this market change often enough that any specific figure published today should be treated as a starting point for negotiation, not a quote.
5. Comparing the gateways honestly
Here is how I actually choose. Fees and settlement timing in this table are described as shapes, not quoted numbers, because every published rate I have seen is a rack rate that moves with volume, category and negotiation. Get your own quote.
| Provider | Type | Core markets | Integration shape | Fee & settlement shape | Best for |
|---|---|---|---|---|---|
| PayTabs | Card gateway | KSA, UAE, Egypt, Kuwait, Oman, Bahrain, Qatar, Jordan and neighbours | Hosted payment page, managed form, or full API; IPN plus per-request callback; body-level HMAC signature | Percentage plus fixed per transaction; settlement typically a small number of business days, faster in KSA than UAE | Multi-country Gulf merchants who want one contract across borders |
| HyperPay | Card gateway | KSA and wider Gulf | COPYandPAY hosted widget or server-to-server; checkout-ID model; 3D Secure handled for you | Enterprise-oriented pricing; heavier onboarding and compliance review | Larger merchants, regulated categories, enterprise procurement |
| Moyasar | Card gateway | KSA | Clean REST API, good docs, straightforward mada and Apple Pay support | Transparent published pricing; fast mada settlement is its standout claim | Saudi startups and developer-led teams that want to ship this month |
| Paymob | Card gateway | Egypt, KSA, UAE, Pakistan | Intention API, integration IDs per method, HMAC on callbacks | Egypt-competitive pricing; local wallets and instalment partners bundled | Stores where Egypt is the primary or a major market |
| Tabby | BNPL | KSA, UAE, Kuwait, Qatar, Bahrain | Checkout session, pre-scoring, authorize then capture; webhook via shared-secret custom header | Merchant discount rate materially higher than card; settlement in days, tiered by plan | Broad Gulf reach and strong UAE presence |
| Tamara | BNPL | KSA, UAE, Kuwait | Checkout, explicit authorise step, capture, 21-day auto-capture; JWT-signed notifications | Merchant discount rate materially higher than card; settlement tiered by plan | Saudi-first stores; deep brand recognition in the Kingdom |
The practical read: card gateways compete on integration quality and settlement speed, and the differences are small enough that developer experience should decide it. BNPL providers compete on approval rate and consumer brand, and the differences there are large enough that many serious Saudi stores run both Tabby and Tamara side by side and let the customer choose.
6. What does BNPL integration cost and how long does it take?
Three separate costs, and people only budget for the first.
Build cost
For a single BNPL provider added to an existing, well-structured custom store — meaning you already have a clean order state machine and a working payment abstraction — a competent developer needs a handful of working days. That covers session creation, redirect handling, the webhook endpoint, capture on fulfilment, refund handling, the product-page widget, admin visibility, and sandbox testing across the failure paths.
Add the second provider and it is faster, because the abstraction already exists — provided you built one. If you hardcoded the first provider into your checkout controller, the second integration costs the same as the first plus a refactor.
If your store does not already have a clean payment abstraction, budget for building one first. That is not BNPL work, it is the foundation, and skipping it is how stores end up with payment logic scattered across six controllers. The principles in API design best practices apply directly here: one interface, provider-specific adapters behind it, no provider names leaking into your domain logic.
Elapsed time
The build is not the constraint. Merchant onboarding is. Between application, document review, category approval, commercial negotiation and production key issuance, plan for weeks rather than days, and start it before development. I tell clients to treat BNPL go-live as a milestone that depends on a third party they do not control, and to launch the store without it if the contract is not signed.
Ongoing cost
This is the one founders underestimate. BNPL merchant discount rates are substantially higher than card processing — the gap is large enough to move your gross margin, and it varies with your plan, your category and your negotiating position. Published ranges from comparison sites sit well above typical card rates, and both providers negotiate. Model it on your actual basket mix before you commit. If your margin is thin and your average order value is low, BNPL can cost you more than the incremental orders are worth. That is the unprofitable thing I say to clients and it is occasionally not what they want to hear.
Run the arithmetic before the integration. Take last quarter's orders. Assume a plausible share shifts to BNPL. Apply the quoted merchant rate to that share. Compare against a realistic estimate of the incremental revenue from higher basket value and improved conversion. If the answer is close, BNPL is a marketing decision, not a payments decision — and it should be evaluated by whoever owns the marketing budget.
7. Can I use Stripe instead of a Gulf gateway in Saudi Arabia?
No, not as a Saudi-registered business.
Saudi Arabia is not among Stripe's supported countries for opening a merchant account. A Saudi entity cannot onboard directly. The workaround people describe — register a company in a Stripe-supported jurisdiction and process through that — is legally and operationally real, but it is a bad idea for a Saudi store, for reasons that have nothing to do with Stripe's product quality:
- No mada. This is disqualifying on its own. You would be turning away the card most of your customers hold.
- Cross-border decline rates. Saudi-issued cards presented to a foreign acquirer decline more often. You do not see this in testing. You see it in your conversion rate.
- Currency and settlement friction. Customers see foreign-transaction fees, you take FX on the way out, and your books stop matching your local tax position.
- Regulatory exposure. Selling into the Kingdom while processing through an offshore entity raises questions about your e-invoicing and tax posture that you do not want to answer retroactively. If you are VAT-registered in Saudi Arabia you also have ZATCA e-invoicing obligations that need to line up with how payments actually settle.
Where Stripe genuinely fits: you are a company registered in a supported jurisdiction, selling digital products globally, and Saudi customers are a minority of revenue. Then Stripe plus a Gulf gateway as a secondary rail can make sense. For a Gulf-first store, it does not.
The same logic applies to PayPal in this region. It is a supplementary method for a specific customer segment, not a primary rail.
8. Webhooks: the part nobody documents properly
This is where custom stores actually break, and it is the section I would have wanted five years ago.
Every provider here sends server-to-server notifications, and every provider authenticates them differently:
- PayTabs sends a signature header containing an HMAC of the entire request body, hashed with your profile's server key. You recompute and compare.
- Paymob sends an HMAC calculated over a specific, ordered concatenation of named fields — not the raw body — which you must build in exactly their documented order. Get the order wrong and every callback fails verification.
- Tamara sends a JWT notification token, both as a query parameter and as a bearer token, signed with HS256, which you decode and validate.
- Tabby lets you register a custom header name and value at webhook registration time, and includes that shared secret in deliveries. Note carefully what this is: a shared secret, not a signature over the payload. It proves the request came from a party holding your secret. It does not prove the body is unmodified.
That last distinction is why the following rule exists.
Rule one: reconcile, do not trust
Never mutate order or money state directly from webhook body contents. Use the notification only to learn that something about this payment ID changed, then call the provider's retrieve endpoint with your secret key and act on that authoritative response.
This costs you one HTTP call. It buys you immunity from spoofed payloads, replayed bodies, truncated deliveries, and every schema change the provider ships without telling you.
Rule two: idempotency is mandatory
Every provider retries. A retry is indistinguishable from a duplicate at your endpoint. Without deduplication you get double fulfilment, double emails, double ledger entries and eventually double refunds.
Deduplicate on a stable identifier — the event ID if the provider sends one, otherwise a hash of payment ID plus resulting status — stored with a unique database constraint. Let the constraint be the enforcement, not an application-level check that races under concurrent delivery.
Rule three: assume out-of-order delivery
A captured notification can arrive before the authorized notification it logically follows. Networks are not ordered. If your handler is a sequence of if-statements that assumes a progression, it will corrupt state the first time delivery is reordered.
Model payment state as an explicit state machine with allowed transitions, and reject transitions that would move a payment backwards. A refunded payment receiving a late authorized notification should log and ignore, not regress.
Rule four: acknowledge fast, process async
Return 200 as soon as you have durably recorded the event. Do the reconciliation, the fulfilment trigger, the invoice generation and the emails on a queue. If you do fulfilment work inline and your handler takes eight seconds, the provider times out, marks delivery failed, and retries — and now you are doing that work twice concurrently.
// Laravel: a webhook endpoint that behaves under retries
public function handle(Request $request, string $provider)
{
$verifier = $this->verifiers->for($provider);
if (! $verifier->verify($request)) {
Log::warning('payment.webhook.rejected', ['provider' => $provider]);
return response()->noContent(401);
}
$eventKey = $verifier->eventKey($request); // stable per logical event
try {
$event = WebhookEvent::create([
'provider' => $provider,
'event_key' => $eventKey, // UNIQUE (provider, event_key)
'payload' => $request->all(),
]);
} catch (QueryException $e) {
// Already seen. Retry or duplicate. Acknowledge and stop.
return response()->noContent(200);
}
ReconcilePayment::dispatch($provider, $verifier->paymentId($request), $event->id);
return response()->noContent(200);
}
And the job that does the real work — note that it asks the provider, rather than believing the payload it was handed:
public function handle(PaymentGatewayRegistry $gateways): void
{
$gateway = $gateways->get($this->provider);
// Authoritative read. The webhook was only a hint.
$remote = $gateway->retrievePayment($this->paymentId);
DB::transaction(function () use ($remote) {
$payment = Payment::where('provider_payment_id', $remote->id)
->lockForUpdate()
->firstOrFail();
if (! $payment->state->canTransitionTo($remote->state)) {
Log::info('payment.transition.ignored', [
'from' => $payment->state->value,
'to' => $remote->state->value,
]);
return;
}
$payment->applyRemoteState($remote);
$payment->save();
});
}
Two more things that belong in every production integration. First, log every inbound webhook raw and keep it — when a provider disputes a settlement, the stored payload and your response code is the evidence. Second, treat the webhook endpoint as an unauthenticated public route and secure it accordingly; it belongs on your website security checklist alongside rate limiting and body-size limits.
The reconciliation job you will regret not building
Webhooks fail. Providers have outages, your server has deploys, DNS has bad days. Run a scheduled job — hourly is usually enough — that finds every payment sitting in a non-terminal state older than a threshold and polls the provider for its current status. This catches everything the webhook layer missed, and it is perhaps thirty lines of code. Every mature payment integration I have worked on has one. Most of the broken ones I have been called to fix did not.
9. Captures, refunds and where the money actually is
Authorization is not payment. Capture is not settlement. Settlement is not reconciliation. Three distinct events, three distinct timestamps, and your finance team cares about all three.
Capture at fulfilment, not at checkout. Both BNPL providers and most card gateways support authorize-then-capture. Capturing at checkout for physical goods means you are holding customer money for items that may be out of stock — operationally messy, and in a refund-heavy category it inflates your chargeback and refund metrics for no reason.
Partial captures need partial thinking. If you ship three of five items, capture three items' worth and handle the remainder explicitly. Do not capture the full amount and refund the difference — it looks identical in your database and completely different in the provider's reporting and your fee bill.
Refunds are not reversals. A refund on a BNPL order does not simply undo the instalment plan. The provider adjusts the consumer's remaining schedule, and consumer-facing refund timelines run into weeks depending on the receiving bank. Set that expectation in your customer-facing copy, in Arabic, or your support inbox will carry the cost.
Settlement is not per-order. Providers batch. You receive a lump sum on a cycle, net of fees, covering a window of transactions. Your database has orders; your bank statement has deposits. Nothing reconciles automatically. Build a settlement import that maps provider settlement reports to order IDs from launch, not eighteen months in when someone finally asks why the numbers do not match. This is a data-modelling problem as much as a payments one, and the fundamentals in database design for web apps apply — payments, captures, refunds and settlements are four related tables, not four columns on the orders table.
Invoice on capture, not on order. If you are VAT-registered in Saudi Arabia, the timing of your tax invoice matters and it should follow the money, not the click. Wire your invoice generation to the capture event. My write-up on ZATCA e-invoicing in Laravel covers the compliance side of that in detail.
10. Which gateway should a Gulf store launch with first?
My recommendation, stated plainly.
Saudi-only store: launch with Moyasar if your team values developer experience and speed to market, or PayTabs if you expect to expand beyond the Kingdom within a year. Either gives you mada, cards and Apple Pay through one integration. Add Tamara first for BNPL, because Saudi brand recognition is strongest there, then Tabby once the first is stable.
UAE-first store: PayTabs or Checkout.com for cards, Tabby first for BNPL given its UAE strength, Tamara after.
Multi-country Gulf from day one: PayTabs, because one contract spanning most GCC markets removes an enormous amount of operational overhead — separate reconciliation per country is a real cost that does not show up in the fee comparison.
Egypt in scope: Paymob for the Egyptian side, and accept that you will run two card gateways. Do not try to force one provider to cover both Egypt and the Gulf well.
In every case: build the provider abstraction before the second provider, not after. The interface you need is small — create session, retrieve payment, capture, partial capture, refund, void — and the discipline of writing it up front is what makes provider number three a two-day job instead of a two-week one.
A test plan worth copying. Before you go live, deliberately exercise: a customer who abandons at the provider's page; a customer who closes the browser after approving but before redirect; a duplicate webhook; a webhook that arrives out of order; a capture that fails; a partial refund followed by a full refund attempt; a session that expires; and a BNPL rejection at pre-scoring. Every one of these happens in the first month of real traffic. Only the last one is documented well by the providers.
11. Mistakes I keep getting hired to fix
Order state driven by the browser redirect. The customer's phone loses signal on the way back from the bank OTP page, the success route never fires, the order sits unpaid, and the customer has been charged. Always let the server-to-server notification plus your reconciliation poll own the state.
Payment logic inside the checkout controller. Four providers, four sets of if-branches, no abstraction, and every change risks all four. This is the single most expensive structural mistake in Gulf ecommerce codebases.
Secret keys in the frontend. Tabby and most gateways issue separate public and secret keys for a reason. The secret key never leaves your server. I still find them in JavaScript bundles.
No sandbox parity. Testing only the happy path in sandbox, then discovering in production that 3D Secure adds a redirect your state machine did not anticipate.
Ignoring the Arabic checkout. Right-to-left layout breaking on the payment step, English-only error messages, and phone-number fields that reject the format Saudi customers actually type. Bilingual is not a translation layer bolted on at the end — it is a checkout requirement in this market.
No idempotency key on outbound calls either. Webhook deduplication gets attention; outbound retry safety does not. If your capture call times out and you retry without an idempotency key, you may capture twice.
Treating the provider dashboard as the ledger. It is a reporting view of their data, not your books. Your system needs its own payment records that you can query, audit and reconcile independently.
12. Where to start
If you are at the beginning: pick one card gateway, get the merchant application in this week, and build the payment abstraction while you wait for approval. Add BNPL when the contract is signed, not before. Build the reconciliation job on day one because you will never find time for it later.
If you have an existing store that is losing orders at checkout, the diagnosis is usually one of three things: missing mada, a state machine that depends on the browser returning, or a webhook handler that is not idempotent. All three are fixable in days, not months.
I build and repair Gulf payment integrations as part of custom ecommerce development, usually on Laravel, and I am equally happy to review an existing integration and tell you what is wrong with it than to rebuild it. If you need someone to own this end to end, you can hire me as your Laravel developer — or just send me the details of your store and I will tell you honestly whether you have a problem worth paying to solve.