Two years ago, I was two weeks behind deadline on a Laravel-based platform for a luxury limo service in Dubai. Their payment gateway integration was stalling everything — the API docs for the local provider were half-translated from Arabic, endpoints kept timing out, and I couldn’t for the life of me figure out why the signature generator kept rejecting valid JSON. Welcome to the world of UAE-specific payment integrations.
This article isn’t going to promise you “seamless” steps or “holistic” solutions. It’s a realistic approach based on battles I’ve fought — sometimes won — with local payment gateways in the UAE and GCC region. Let’s work through a basic implementation using actual code samples and real frustrations you might hit.
**Getting Started: What You'll Need**
When a client in Abu Dhabi says “add payments,” they usually mean a combo of:
- •UAE bank card processors (Mada, FawryPay)
- •Regional mobile wallets (Thawani, Wibcash)
- •Sometimes even cash-on-delivery for SMEs
I picked Thawani as a test case for this guide — they’re gaining traction in GCC e-commerce circles, and I had a real project (more on that later) where they caused some late-night coding sessions.
You’ll need:
- •A Laravel 9+ project with PHP 8.1
- •A testing environment (use ngrok.io for callback URLs)
- •Thawani sandbox credentials (or equivalent gateway if you’ve got a preferred local provider)
**Setting Up the Gateway**
I start by creating a PaymentServiceProvider to keep the madness contained. Run this in your terminal:
php artisan make:service-provider PaymentServiceProviderThen in config/services.php, add:
'thawani' => [
'public_key' => env('THAWANI_PUBLIC_KEY'),
'secret_key' => env('THAWANI_SECRET_KEY'),
'base_url' => env('THAWANI_BASE_URL', 'https://uatcheckout.thawani.om/api'),
'currency' => 'OMR'
];Pro tip: Don’t hardcode the currency as AED — many Gulf-based gateways have OMR (Omani Rial) as default. Learned this the hard way when 10K AED showed up as 3,000 Rial in a test transaction.
**Dealing With Authentication**
This part sucked. Their API expects your secret key in a custom header called X-Thawani-Secret. Laravel’s default Guzzle client doesn’t handle that neatly.
Here’s how I wrapped it in a service class:
class ThawaniPaymentHandler {
protected $client;
public function __construct() {
$this->client = new \GuzzleHttp\Client([
'base_uri' => config('services.thawani.base_url'),
'headers' => [
'X-Thawani-Secret' => config('services.thawani.secret_key')
]
]);
}
}Now about that time my SSL certificate kept getting rejected. The sandbox server had an intermediate cert that Ubuntu didn’t trust — a common problem in local dev environments but a nightmare in production. I ended up disabling cert verification in the dev config (DON’T DO THIS IN PROD):
'verify' => app()->isLocal() ? false : trueYeah. Not my proudest decision, but clients don’t care about certificate chains when their site’s down.
**Initiating a Payment**
Let’s say you’re building a luxury car booking platform — been there, did that for a client in Sharjah. Here’s how you generate a payment link:
public function createCheckout(Request $request): JsonResponse {
$payload = [
'client_reference_id' => $booking->id,
'products' => [[
'name' => 'Toyota Limousine',
'quantity' => 1,
'unit_amount' => $booking->total_amount * 1000 // they use 3 decimal places
]]
];
$response = $this->client->post('/v1/charges/create', [
'json' => $payload,
'headers' => [
'Accept' => 'application/json'
]
]);
$data = json_decode($response->getBody(), true);
return redirect()->away($data['session']['checkout_url']);
}They require all amounts in fils, even if you set the currency as OMR. Multiply all your numbers by 1,000 or deal with 0.5 Rials magically turning into half a cent (yes, that happened on a test transaction).
**Handling the Callback**
This part tripped me up when a client insisted on real-time order updates in Arabic. They wanted instant notifications through a push service — ended up using Firebase FCM with a Laravel echo-server for webhooks.
public function handleWebhook(Request $request): \Illuminate\Http\Response {
$payload = json_decode($request->getContent(), true);
if ($payload['type'] === 'charge.succeeded') {
// Update database
Booking::findOrFail($payload['data']['client_reference_id'])->update([
'payment_status' => 'paid',
'receipt_url' => $payload['data']['receipt_url']
]);
}
return response()->noContent(200);
}Make sure your webhook URL is HTTPS (they’ll reject HTTP with a 403), and test your ngrok tunnel by pinging it from your server via curl:
curl https://your-ngrok-url.onrender.com/webhook**Real-World Example: Tawasul Limo Booking**
I used this exact implementation for Tawasul Limo — a luxury car booking platform that needed real-time payments in AED and OMR. Their Emirati clients preferred local mobile wallets, but the business operates in 5 GCC countries. We had to handle Arabic number formatting, 12-hour time zones for SMS confirmations, and support partial refunds — which the gateway only allowed through a CSV upload until their API v2 release.
The refund function had to wait until midnight UAE time because their cron jobs ran at 4am. Yeah, not ideal, but clients don't pay for what’s ideal.
**Testing It All**
Run your tests with Laravel Dusk for browser flows and use this Artisan command for backend checks:
php artisan make:test PaymentIntegrationTest --unitThen in the test:
public function test_thawani_returns_charge_id() {
$handler = new ThawaniPaymentHandler();
$payload = require 'your-payment-data.php';
$response = $handler->createCharge($payload);
$this->assertStringStartsWith('chg_', $response['id']);
}Remember to check for duplicate transaction IDs — their API errors out if you try two identical payment requests within 15 minutes. We ended up storing hashes of recent requests in Redis with a 20-minute expiry.
Honestly, these local integrations are never going to be as clean as Stripe. But when a family-owned restaurant in Jeddah needs to accept payments through a specific Saudi digital wallet, you make it work — even if the documentation has broken images and 3 versions of Arabic headers.
If you're stuck on a particular part of this process, feel free to reach out through my contact form. I'm always up for coffee and API war stories.