mustafataj / tabby-laravel
Laravel package for Tabby Buy Now Pay Later checkout, payments, and webhooks
Requires
- php: ^8.2
- illuminate/console: ^10.0|^11.0|^12.0|^13.0
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/events: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/routing: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- illuminate/view: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.0|^11.0|^12.0|^13.0
README
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