laravel-tabby maintained by aghfatehi
Laravel Tabby Pay in 4 Payment Gateway
A professional Laravel package for integrating Tabby Pay in 4 - the leading Buy Now Pay Later (BNPL) solution in the Middle East. Supports Saudi Arabia, UAE, and Kuwait.
Customers can split their payments into 4 interest-free installments, increasing conversion and average order value.
Features
- ✅ Full Tabby Checkout flow (Create Session, Callback, Cancel, Failure)
- ✅ Capture authorized payments
- ✅ Refund payments (partial or full)
- ✅ Payment details retrieval
- ✅ Webhook management (authorized, captured, failed, rejected)
- ✅ Sandbox & Production environments
- ✅ Multi-region support (SA, AE, KW)
- ✅ Multi-currency support (SAR, AED, KWD)
- ✅ Arabic & English language support
- ✅ Native PHP cURL client (no external HTTP dependencies)
- ✅ Configurable routes prefix & middleware
- ✅ Transaction logging migration
- ✅ Laravel 10, 11, 12 & 13 compatible
- ✅ PHP 8.1+
Requirements
| Laravel | PHP | Package Version |
|---|---|---|
| 10.x | ^8.1 | ^1.0 |
| 11.x | ^8.2 | ^1.0 |
| 12.x | ^8.2 | ^1.0 |
| 13.x | ^8.2 | ^1.0 |
Installation
composer require aghfatehi/laravel-tabby
Configuration
1. Publish Configuration
php artisan vendor:publish --tag=tabby-config
2. Publish Migration (Optional)
php artisan vendor:publish --tag=tabby-migrations
php artisan migrate
3. Environment Variables
Add these to your .env file:
# ─── Tabby Payment Gateway Environment Variables ──────────────────────────────
TABBY_SANDBOX_MODE=true
# bool | Sandbox mode (testing) when true, Production mode when false
TABBY_SECRET_KEY="sk_test_your_secret_key_here"
# string | Your Tabby secret key from Tabby dashboard
# Prefix: sk_test_ for sandbox | sk_live_ for production
TABBY_MERCHANT_CODE="your_merchant_code_here"
# string | Your merchant code from Tabby dashboard
TABBY_REGION="sa"
# string | Region determines API endpoint
# "sa" = https://api.tabby.sa | "ae" = https://api.tabby.ai | "kw" = https://api.tabby.ai
TABBY_CURRENCY="SAR"
# string (3 chars) | Currency code
# SAR = Saudi Riyal | AED = UAE Dirham | KWD = Kuwaiti Dinar
TABBY_LANGUAGE="ar"
# string | Checkout widget language
# "en" = English | "ar" = Arabic
TABBY_LOGGING=true
# bool | Log all API requests/responses to laravel.log
# true = enabled | false = disabled
4. Service Provider
The package auto-discovers via Laravel's package discovery. If you disable discovery, register manually in config/app.php:
'providers' => [
Aghfatehi\Tabby\TabbyServiceProvider::class,
],
5. Facade (Optional)
'aliases' => [
'Tabby' => Aghfatehi\Tabby\Facades\Tabby::class,
],
Usage
Quick Start - Frontend Checkout
use Aghfatehi\Tabby\Facades\Tabby;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redirect;
/**
* Example controller method showing the full Tabby checkout payload
* with explicit default values, data types, and field descriptions.
*/
try {
// ═══════════════════════════════════════════════════════════════════
// 1. PAYMENT DATA
// ═══════════════════════════════════════════════════════════════════
$amount = 500.00;
// float | Order total (e.g. 500.00)
// Tabby requires minimum 50 AED or equivalent in other currencies
$currency = 'SAR';
// string (3 chars) | Payment currency
// 'SAR' = Saudi Riyal | 'AED' = UAE Dirham | 'KWD' = Kuwaiti Dinar
// Must match the currency registered in your Tabby account
$user = request()->user();
// ?\Illuminate\Foundation\Auth\User | Authenticated user (may be null)
// ─── Buyer Information ───────────────────────────────────────────
$firstName = $user?->name ?? 'Ahmed';
// string | Buyer's first name - falls back to default if user not logged in
$lastName = $user?->name ?? 'Ali';
// string | Buyer's last name
$fullName = trim("$firstName $lastName") ?: 'Ahmed Ali';
// string | Full name (max 255 chars)
$email = $user?->email ?? 'customer@example.com';
// string | Email address (must be valid for payment notifications)
// Test: use otp.success@tabby.ai to always succeed
$phone = $user?->phone ?? '500000001';
// string | Phone number WITHOUT country prefix (+)
// e.g. "500000001" for Saudi Arabia, "500000001" for UAE
// Test: "500000001" succeeds, "500000000" fails
$dob = $user?->dob ?? '1990-01-01';
// string (YYYY-MM-DD) | Buyer's date of birth (optional, helps approval)
// ─── Order References ────────────────────────────────────────────
$orderReferenceId = $request->input('reference_id', 'ORD-' . uniqid());
// string | Your unique order reference (max 255 chars)
// Used to link your order with Tabby's payment
// Pass via POST request to routes: tabby.pay, tabby.callback, tabby.cancel, tabby.failure
$orderId = $request->input('order_id', uniqid('ord_'));
// string | Order ID displayed in Tabby's checkout UI
// Pass via POST request to routes: tabby.pay
// ═══════════════════════════════════════════════════════════════════
// 2. CHECKOUT PAYLOAD
// ═══════════════════════════════════════════════════════════════════
$requestBody = [
// ─── Payment ────────────────────────────────────────────────────
'payment' => [
'amount' => (string) $amount,
// string | Total amount as string - "500.00"
// IMPORTANT: Tabby requires amount as string, NOT float
// Must be >= 50 AED or equivalent
'currency' => $currency,
// string (3 chars) | Currency - "SAR" | "AED" | "KWD"
'description' => 'Payment for order #1234',
// string | Short payment description (max 255 chars)
// Displayed to the buyer in Tabby's checkout
// ── Buyer ─────────────────────────────────────────────────────
'buyer' => [
'phone' => $phone,
// string | Phone number - "500000001"
// WITHOUT country prefix (+966)
'email' => $email,
// string | Email - "customer@example.com"
'name' => $fullName,
// string | Full name - "Ahmed Ali"
'dob' => $dob,
// string (YYYY-MM-DD) | Date of birth - "1990-01-01"
],
// ── Buyer History (optional, improves approval rate) ───────
'buyer_history' => [
'registered_since' => $user?->created_at?->format('Y-m-d\TH:i:s\Z')
?? '2024-01-01T00:00:00Z',
// string (ISO8601) | Customer registration date
// Older registration = higher approval chance
'loyalty_level' => 0,
// int | Loyalty tier (0 = new, 1 = regular, 2 = VIP)
'wishlist_count' => 0,
// int | Number of items in wishlist
'is_social_networks_connected' => false,
// bool | Does customer have social accounts linked?
'is_phone_number_verified' => !empty($phone),
// bool | Is phone number verified?
'is_email_verified' => !empty($email),
// bool | Is email verified?
],
// ── Order ───────────────────────────────────────────────────
'order' => [
'tax_amount' => '0.00',
// string | Tax amount - "0.00" or actual tax
'shipping_amount' => '0.00',
// string | Shipping cost - "0.00" or actual cost
'discount_amount' => '0.00',
// string | Discount amount - "0.00" or actual discount
'updated_at' => now()->format('Y-m-d\TH:i:s\Z'),
// string (ISO8601) | Order last updated timestamp
'reference_id' => $orderReferenceId,
// string | Your order reference - "ORD-abc123"
// ── Order Items ─────────────────────────────────────────
'items' => [
[
'title' => 'Wireless Headphones',
// string | Product name (required)
'description' => 'Bluetooth 5.0',
// string | Product description (optional)
'quantity' => 1,
// int | Quantity (must be >= 1)
'unit_price' => '500.00',
// string | Unit price as string
'discount_amount' => '0.00',
// string | Discount on this item
'reference_id' => 'SKU-001',
// string | SKU or product ID in your system
'image_url' => 'https://example.com/headphones.jpg',
// string (URL) | Product image URL (optional)
'product_url' => 'https://example.com/products/1',
// string (URL) | Product page URL (optional)
'category' => 'Electronics',
// string | Product category (optional)
],
],
],
// ── Shipping Address ────────────────────────────────────────
'shipping_address' => [
'city' => 'Riyadh',
// string | City - e.g. "Riyadh" or "Dubai"
'address' => '3764 Al Urubah Rd',
// string | Street address
'zip' => '12345',
// string | ZIP / postal code
],
// ── Meta ────────────────────────────────────────────────────
'meta' => [
'order_id' => $orderId,
// string | Your internal order ID
'customer' => (string) ($user?->id ?? 'guest'),
// string | Customer ID in your system
],
],
// ─── Session Configuration ─────────────────────────────────────
'lang' => config('tabby.language', 'en'),
// string | Checkout language - "en" = English | "ar" = Arabic
'merchant_code' => config('tabby.merchant_code', ''),
// string | Merchant code in Tabby - "ae" | "sa" | "kw"
// Must match the code registered in your Tabby account
// ─── Merchant Callback URLs ───────────────────────────────────
'merchant_urls' => [
'success' => route('tabby.callback'),
// string (URL) | Redirect browser here after successful payment
// Receives payment_id in query string
'cancel' => route('tabby.cancel'),
// string (URL) | Redirect browser here if user cancels
'failure' => route('tabby.failure'),
// string (URL) | Redirect browser here if payment fails
],
];
// ═══════════════════════════════════════════════════════════════════
// 3. SEND REQUEST (Submit to Tabby API)
// ═══════════════════════════════════════════════════════════════════
if (config('tabby.logging', true)) {
Log::info('Tabby Checkout Request:', $requestBody);
}
$response = Tabby::createCheckout($requestBody);
// array | API response contains:
// ['payment']['id'] => "pay_xxxxxxxx" (payment ID)
// ['id'] => "uuid-string" (session ID)
// ['configuration']['available_products']['installments'][0]['web_url'] => checkout URL
if (config('tabby.logging', true)) {
Log::info('Tabby Checkout Response:', $response);
}
// ═══════════════════════════════════════════════════════════════
// 4. HANDLE RESPONSE
// ═══════════════════════════════════════════════════════════════
if (isset($response['error']) || isset($response['errors'])) {
// Tabby returned an error - show it to the user
$error = $response['message']
?? $response['errors'][0]['message']
?? 'Payment initialization failed';
return back()->withErrors(['error' => $error]);
}
$paymentId = $response['payment']['id'] ?? null;
// string|null | "pay_xxxxxxxx" - Tabby payment ID (store in DB)
$sessionId = $response['id'] ?? null;
// string|null | UUID - checkout session ID
// Extract checkout URL from available products
$webUrl = $response['configuration']['available_products']['installments'][0]['web_url']
?? $response['configuration']['available_products']['pay_by_installments']['web_url']
?? null;
// string|null | URL to redirect buyer to Tabby checkout
// e.g. "https://checkout.tabby.ai/..."
// Store in session for callback verification
session([
'tabby_payment_id' => $paymentId,
// string|null | Used in callback to verify payment status
'tabby_session_id' => $sessionId,
// string|null | Used in webhook to match the session
]);
if ($webUrl) {
return Redirect::away($webUrl);
// Redirect buyer to Tabby checkout page
}
return back()->withErrors(['error' => 'No checkout URL returned']);
// Unexpected: no checkout URL in response
} catch (\Throwable $e) {
// API connection error (Network, Timeout, etc.)
Log::error('Tabby Checkout Exception: ' . $e->getMessage());
return back()->withErrors(['error' => $e->getMessage()]);
}
Using Routes
The package registers these routes under the configured prefix (/tabby by default):
| Method | URI | Name | Description |
|---|---|---|---|
| POST | /tabby/pay |
tabby.pay |
Initiate checkout |
| ANY | /tabby/callback |
tabby.callback |
Payment callback |
| GET | /tabby/cancel |
tabby.cancel |
Cancel handler |
| GET | /tabby/failure |
tabby.failure |
Failure handler |
| POST | /tabby/webhook |
tabby.webhook |
Webhook receiver |
| POST | /tabby/capture |
tabby.capture |
Capture payment |
| POST | /tabby/refund |
tabby.refund |
Refund payment |
| GET | /tabby/payment/{id} |
tabby.payment.details |
Payment details |
Request Parameters
| Route | Parameter | Type | Required | Description |
|---|---|---|---|---|
tabby.pay |
amount |
number | yes | Order total |
reference_id |
string | no | Your unique order reference | |
order_id |
string | no | Internal order ID | |
description |
string | no | Payment description | |
phone |
string | no | Buyer phone (without + prefix) | |
email |
string | no | Buyer email | |
items |
array | no | Order items array | |
tax_amount |
string | no | Tax amount | |
shipping_amount |
string | no | Shipping amount | |
discount_amount |
string | no | Discount amount | |
merchant_urls |
object | no | Custom callback URLs (success, cancel, failure) | |
tabby.callback |
payment_id |
string | no | Tabby payment ID (auto from Tabby redirect) |
reference_id |
string | no | Your order reference (auto from redirect URL) | |
tabby.capture |
payment_id |
string | yes | Tabby payment ID to capture |
amount |
number | yes | Amount to capture | |
tabby.refund |
payment_id |
string | yes | Tabby payment ID to refund |
amount |
number | yes | Amount to refund |
Example - initiating checkout via route:
use Illuminate\Support\Facades\Http;
$response = Http::post('/tabby/pay', [
'amount' => 500.00,
'reference_id' => 'ORD-12345',
'order_id' => 'ORD-12345',
'description' => 'Payment for order #12345',
'phone' => '500000001',
'email' => 'otp.success@tabby.ai',
'merchant_urls' => [
'success' => route('tabby.callback', ['reference_id' => 100]),
'cancel' => route('tabby.cancel', ['reference_id' => 100]),
'failure' => route('tabby.failure', ['reference_id' => 100]),
],
]);
API Methods
use Aghfatehi\Tabby\Facades\Tabby;
// Create Checkout Session
$checkout = Tabby::createCheckout($data);
// Retrieve Payment
$payment = Tabby::getPayment('pay_xxxxx');
// Update Payment (e.g., order reference)
$updated = Tabby::updatePayment('pay_xxxxx', [
'order' => ['reference_id' => 'NEW-ORD-123'],
]);
// Capture Payment (after authorization)
$captured = Tabby::capturePayment('pay_xxxxx', '500.00');
// Refund Payment
$refunded = Tabby::refundPayment('pay_xxxxx', '100.00');
// List Payments with filters
$payments = Tabby::listPayments([
'created_at__gte' => '2025-03-01',
'limit' => 20,
]);
// Webhook Management
$webhook = Tabby::webhookRegister('https://example.com/webhook', [
'title' => 'Authorization',
'value' => 'Bearer xxx',
]);
$list = Tabby::webhookList();
$detail = Tabby::webhookGet('webhook-id');
Tabby::webhookUpdate('webhook-id', 'https://example.com/webhook-new');
Tabby::webhookDelete('webhook-id');
Amount Formatting
use Aghfatehi\Tabby\Facades\Tabby;
$formatted = Tabby::formatAmount(100.5); // "100.50"
Regions & API Endpoints
| Region | Code | Sandbox URL | Production URL |
|---|---|---|---|
| Saudi Arabia | sa | https://api.tabby.sa |
https://api.tabby.sa |
| UAE | ae | https://api.tabby.ai |
https://api.tabby.ai |
| Kuwait | kw | https://api.tabby.ai |
https://api.tabby.ai |
Webhook Events
The webhook endpoint handles these events:
payment_authorized- Payment has been authorized (ready to capture)payment_captured- Payment has been captured (funds collected)payment_failed- Payment failedpayment_rejected- Payment was rejected
Customising Routes
Publish the config and modify the routes section:
// config/tabby.php
'routes' => [
'prefix' => 'payment/tabby', // Custom prefix
'middleware' => ['web', 'auth'], // Custom middleware
],
Tabby Testing Credentials
UAE: otp.success@tabby.ai, phone: +971500000001 KSA: otp.success@tabby.ai, phone: +966500000001 Kuwait: otp.success@tabby.ai, phone: +96590000001
otp test: 8888
Test Cards (Sandbox)
| Card Type | Number | Expiry | CVV |
|---|---|---|---|
| Visa | 4508750015741019 | 07/39 | 100 |
| Mastercard | 5123450000000008 | 07/39 | 100 |
| AMEX | 345678901234564 | 07/39 | 1000 |
Testing
composer test
Changelog
See CHANGELOG for recent changes.
Security
If you discover security issues, please email fathi.a.n2002@gmail.com instead of using the issue tracker.
License
This package is open-sourced software licensed under the MIT license.
Support
- Issues: GitHub Issues
- Tabby Docs: https://docs.tabby.ai
- Author: AL-AGHBARI Fatehi (fathi.a.n2002@gmail.com)