tabby-laravel maintained by mustafataj
Tabby Laravel
Laravel package for Tabby Buy Now Pay Later: checkout sessions, payment capture/refund, and webhooks. Supports sandbox and production, plus regional APIs for UAE/Kuwait (api.tabby.ai) and KSA (api.tabby.sa).
Requirements
- PHP 8.2+
- Laravel 10, 11, 12, or 13
Installation
composer require mustafataj/tabby-laravel
Publish the config:
php artisan vendor:publish --tag=tabby-config
Configuration
Add credentials to .env:
TABBY_ENV=sandbox
TABBY_REGION=ai
TABBY_SANDBOX_PUBLIC_KEY=pk_test_xxx
TABBY_SANDBOX_SECRET_KEY=sk_test_xxx
TABBY_SANDBOX_MERCHANT_CODE=AE
TABBY_PRODUCTION_PUBLIC_KEY=pk_xxx
TABBY_PRODUCTION_SECRET_KEY=sk_xxx
TABBY_PRODUCTION_MERCHANT_CODE=AE
TABBY_CURRENCY=AED
TABBY_CAPTURE_ON_AUTHORIZE=true
TABBY_WEBHOOK_AUTH_HEADER=replace-with-a-long-random-secret
TABBY_WEBHOOK_AUTH_HEADER_NAME=X-Tabby-Webhook-Token
TABBY_WEBHOOK_REQUIRE_AUTH=true
# TABBY_WEBHOOK_ENFORCE_IP=true
| Variable | Description |
|---|---|
TABBY_ENV |
sandbox or production — selects which key pair is used |
TABBY_REGION |
ai (UAE/KW) or sa (KSA) |
TABBY_CAPTURE_ON_AUTHORIZE |
Auto-capture after verify/webhook when payment is AUTHORIZED with no prior captures |
TABBY_WEBHOOK_ENABLED |
Register the incoming webhook route (default true) |
TABBY_WEBHOOK_PATH |
Incoming webhook path (default tabby/webhook) |
TABBY_WEBHOOK_URL |
Absolute URL registered with Tabby (optional) |
TABBY_WEBHOOK_AUTH_HEADER |
Shared secret Tabby sends on webhook POSTs (required when REQUIRE_AUTH=true) |
TABBY_WEBHOOK_AUTH_HEADER_NAME |
Header name registered with Tabby (default X-Tabby-Webhook-Token) |
TABBY_WEBHOOK_REQUIRE_AUTH |
Reject unauthenticated webhooks (default true) |
TABBY_WEBHOOK_ENFORCE_IP |
Restrict webhooks to Tabby IP allowlist |
TABBY_WEBHOOK_THROTTLE |
Laravel throttle string, e.g. 60,1 (set empty to disable) |
Environment is determined by the API keys you send (sk_test_… vs sk_…). Base URLs do not change between sandbox and production.
Usage
Eligibility check
use MustafaTaj\Tabby\Facades\Tabby;
use MustafaTaj\Tabby\Data\Buyer;
use MustafaTaj\Tabby\Data\SessionRequest;
$result = Tabby::checkout()->checkEligibility(new SessionRequest(
amount: '340.00',
currency: 'SAR',
buyer: new Buyer(email: 'customer@example.com', phone: '500000001'),
));
if ($result->isEligible()) {
// Show Tabby at checkout
}
Create session and redirect
use MustafaTaj\Tabby\Data\Buyer;
use MustafaTaj\Tabby\Data\BuyerHistory;
use MustafaTaj\Tabby\Data\MerchantUrls;
use MustafaTaj\Tabby\Data\Order;
use MustafaTaj\Tabby\Data\OrderHistory;
use MustafaTaj\Tabby\Data\OrderItem;
use MustafaTaj\Tabby\Data\SessionRequest;
use MustafaTaj\Tabby\Facades\Tabby;
$session = Tabby::checkout()->createSession(new SessionRequest(
amount: '340.00',
currency: 'SAR',
buyer: new Buyer(
email: 'customer@example.com',
phone: '500000001',
name: 'Jane Doe',
),
order: new Order(
referenceId: (string) $order->id,
items: [
new OrderItem(title: 'Product', quantity: 1, unitPrice: '340.00', category: 'general'),
],
),
merchantUrls: new MerchantUrls(
success: route('checkout.success'),
cancel: route('checkout.cancel'),
failure: route('checkout.failure'),
),
// Optional — improves approval rates when available
buyerHistory: new BuyerHistory(
registeredSince: $customer->created_at->toIso8601String(),
loyaltyLevel: 1,
isPhoneNumberVerified: true,
isEmailVerified: true,
),
orderHistory: [
new OrderHistory(
purchasedAt: '2025-01-15T10:00:00+03:00',
amount: '150.00',
status: 'complete',
paymentMethod: 'card',
buyer: new Buyer(email: 'customer@example.com', phone: '500000001', name: 'Jane Doe'),
items: [
new OrderItem(title: 'Previous item', quantity: 1, unitPrice: '150.00'),
],
),
],
));
// Persist $session->paymentId against your order, then redirect:
return redirect()->away($session->webUrl);
Verify and capture (return URL)
Never trust the redirect alone — always verify server-side:
$payment = Tabby::payments()->verifyAndCapture($paymentId);
if ($payment->isClosed() || $payment->isAuthorized()) {
// Fulfill order
}
Refund
Tabby::payments()->refund($paymentId, '50.00', 'refund-'.$orderId, 'Customer request');
Promo snippet
<x-tabby::promo :price="$product->price" currency="SAR" lang="en" />
Ensure your layout yields @stack('scripts').
Official payment method name: Pay later with Tabby / ادفع لاحقًا مع تابي.
Webhooks
Incoming route: POST /tabby/webhook (no web middleware / CSRF; protected by tabby.webhook auth + optional IP allowlist + throttle).
Auth is required by default. Set TABBY_WEBHOOK_AUTH_HEADER, then register so Tabby sends that header:
php artisan tabby:webhooks:register
# or
php artisan tabby:webhooks:register --url=https://your-app.test/tabby/webhook
Registration includes header.title / header.value from your config so Tabby signs each POST.
Listen for events in your app:
use MustafaTaj\Tabby\Events\PaymentAuthorized;
use MustafaTaj\Tabby\Events\PaymentClosed;
use MustafaTaj\Tabby\Events\PaymentRejected;
use MustafaTaj\Tabby\Events\PaymentExpired;
use MustafaTaj\Tabby\Events\PaymentRefunded;
Event behaviour:
PaymentAuthorized— first authorize notification (no captures yet). WithTABBY_CAPTURE_ON_AUTHORIZE=true, the package also POSTs a capture (idempotentreference_id). Fulfill on this event or onPaymentClosed.- Capture notifications (
authorized+captures[]) are ignored for re-dispatch / re-capture. PaymentClosed— Tabby close webhook after capture settles.PaymentRefunded— payload includes a non-emptyrefundsarray.- Processing failures return HTTP 500 so Tabby retries.
Make fulfillment handlers idempotent — Tabby may deliver duplicates.
Going live
- Complete Tabby’s testing checklist
- Set
TABBY_ENV=productionand live keys - For KSA merchants set
TABBY_REGION=sa - Re-register webhooks with the production secret key
Docs
License
MIT