mustafataj/tabby-php

PHP and Laravel SDK for Tabby Pay in 4 Custom API

Maintainers

Package info

github.com/TajSoft-Plugins/tabby-php

pkg:composer/mustafataj/tabby-php

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.3.2 2026-06-12 13:02 UTC

This package is auto-updated.

Last update: 2026-06-12 13:06:05 UTC


README

Latest Version on Packagist Total Downloads License

Production-ready PHP SDK for Tabby Pay in 4 Custom API. Works in plain PHP and Laravel 10+ out of the box.

Introduction

This package provides a framework-agnostic core with first-class Laravel integration for:

  • Creating checkout sessions
  • Retrieving and updating payments
  • Capturing and refunding payments
  • Listing payments
  • Managing webhooks

The SDK uses Guzzle by default and supports injecting a custom HTTP client through HttpClientInterface.

API keys

Tabby uses two credentials:

Key Used for
Public key (pk_test_... / pk_...) Checkout session creation
Secret key (sk_test_... / sk_...) Payments and webhooks

The SDK selects the correct key automatically per endpoint. Use sandbox keys while testing and live keys in production.

Installation

composer require mustafataj/tabby-php

Laravel Setup

The package auto-registers via Laravel package discovery:

  • Service provider: MustafaTaj\Tabby\Laravel\TabbyServiceProvider
  • Facade alias: Tabby

No manual registration is required for Laravel 10, 11, or 12.

Publishing Config

php artisan vendor:publish --tag=tabby-config

This publishes config/tabby.php to your application.

Environment Variables

Add the following to your .env:

IS_TABBY_SANDBOX=true

TABBY_LIVE_SECRET_KEY=sk_xxx
TABBY_LIVE_PUBLIC_KEY=pk_xxx
TABBY_SANDBOX_SECRET_KEY=sk_test_xxx
TABBY_SANDBOX_PUBLIC_KEY=pk_test_xxx

TABBY_MERCHANT_CODE=your_merchant_code
TABBY_REGION=ksa
TABBY_BASE_URL=
TABBY_TIMEOUT=30
TABBY_CONNECT_TIMEOUT=10
TABBY_HTTP_DEBUG=false

When IS_TABBY_SANDBOX=true, the SDK uses TABBY_SANDBOX_* keys. When false, it uses TABBY_LIVE_* keys.

Optional legacy overrides (used only if the active environment keys are empty):

TABBY_SECRET_KEY=
TABBY_PUBLIC_KEY=

Plain PHP Setup

<?php

require __DIR__.'/vendor/autoload.php';

use MustafaTaj\Tabby\Config\Region;
use MustafaTaj\Tabby\Tabby;

$tabby = Tabby::make([
    'sandbox' => true,
    'keys' => [
        'sandbox' => [
            'secret_key' => 'sk_test_xxx',
            'public_key' => 'pk_test_xxx',
        ],
        'live' => [
            'secret_key' => 'sk_xxx',
            'public_key' => 'pk_xxx',
        ],
    ],
    'merchant_code' => 'your_merchant_code',
    'region' => Region::KSA,
]);

$session = $tabby->checkout()->create([...]); // uses public key
$payment = $tabby->payments()->retrieve('payment_id_here'); // uses secret key

Or pass keys directly without the nested structure:

$tabby = Tabby::make([
    'secret_key' => 'sk_test_xxx',
    'public_key' => 'pk_test_xxx',
    'merchant_code' => 'your_merchant_code',
    'region' => Region::KSA,
]);

You can also load configuration from environment variables:

$tabby = Tabby::fromEnv();

Region and Base URL

Region Value Base URL
KSA ksa https://api.tabby.sa
UAE uae https://api.tabby.ai
Kuwait kuwait https://api.tabby.ai

If base_url is explicitly configured, it overrides the region mapping.

Checkout Session Example

use MustafaTaj\Tabby\Facades\Tabby;

$session = Tabby::checkout()->create([
    'payment' => [
        'amount' => '100.00',
        'currency' => 'SAR',
        'description' => 'Order #1001',
        'buyer' => [
            'phone' => '500000001',
            'email' => 'otp.success@tabby.ai',
            'name' => 'Test Customer',
        ],
        'order' => [
            'reference_id' => '1001',
            'items' => [
                [
                    'title' => 'Product name',
                    'quantity' => 1,
                    'unit_price' => '100.00',
                    'reference_id' => 'SKU-001',
                ],
            ],
        ],
    ],
    'lang' => 'en',
    'merchant_urls' => [
        'success' => route('checkout.success'),
        'cancel' => route('checkout.cancel'),
        'failure' => route('checkout.failure'),
    ],
]);

If merchant_code is omitted from the payload, it is injected from config automatically.

Redirect to Hosted Payment Page

$webUrl = Tabby::checkout()->webUrl($session);

if ($webUrl) {
    return redirect()->away($webUrl);
}

// Or use the helper directly:
use MustafaTaj\Tabby\Support\CheckoutSession;

$webUrl = CheckoutSession::webUrl($session);
$paymentId = CheckoutSession::paymentId($session);

Retrieve Payment Example

use MustafaTaj\Tabby\Facades\Tabby;

$payment = Tabby::payments()->retrieve($paymentId);

Dependency Injection

use MustafaTaj\Tabby\TabbyClient;

class PaymentController
{
    public function show(TabbyClient $tabby, string $paymentId)
    {
        return response()->json(
            $tabby->payments()->retrieve($paymentId)
        );
    }
}

Success Payment Callback Example

After the customer returns from Tabby's hosted payment page, verify the payment and capture it in one call:

use MustafaTaj\Tabby\Facades\Tabby;

$result = Tabby::payments()->retrieveAndCapture(
    paymentId: $paymentId,
    referenceId: 'capture-order-1001',
);

if ($result['successful']) {
    // Fulfill the order
}

// $result shape:
// [
//     'payment' => [...],    // latest payment object from Tabby
//     'captured' => true,    // true when a capture request was sent in this call
//     'capture' => [...],    // capture response, or null when not captured
//     'status' => 'CLOSED',
//     'successful' => true,  // true for AUTHORIZED or CLOSED payments
// ]

retrieveAndCapture() retrieves the payment first. If the status is AUTHORIZED, it captures the full payment amount (or a custom amount you pass). If the payment is already CLOSED, it returns the payment without sending another capture request.

Close Payment Example

Use this when an order is fully cancelled and should not be captured:

Tabby::payments()->close($paymentId);

Payment Status Helper

use MustafaTaj\Tabby\Enums\PaymentStatus;

$status = PaymentStatus::tryFromMixed($payment['status']);

if ($status?->isCapturable()) {
    Tabby::payments()->capture($paymentId, $payment['amount']);
}

if ($status?->isSuccessful()) {
    // Payment is authorized or closed
}

Capture Payment Example

Tabby::payments()->capture(
    paymentId: $paymentId,
    amount: '100.00',
    referenceId: 'capture-order-1001'
);

Refund Payment Example

Tabby::payments()->refund(
    paymentId: $paymentId,
    amount: '50.00',
    referenceId: 'refund-order-1001-1'
);

List Payments Example

use MustafaTaj\Tabby\DTO\Payment\ListPaymentsQuery;

$payments = Tabby::payments()->list(new ListPaymentsQuery(
    createdAtGte: '2024-01-01T00:00:00Z',
    createdAtLte: '2024-12-31T23:59:59Z',
    limit: 20,
    offset: 0,
));

// Or with a raw array:
$payments = Tabby::payments()->list([
    'created_at__gte' => '2024-01-01T00:00:00Z',
    'limit' => 20,
]);

Update Payment Example

Tabby::payments()->update($paymentId, [
    'reference_id' => 'updated-order-reference',
]);

Webhook Registration Example

Tabby::webhooks()->register(
    url: 'https://example.com/webhooks/tabby',
    header: [
        'title' => 'X-Webhook-Secret',
        'value' => 'my-secret',
    ]
);

Webhook requests automatically include the X-Merchant-Code header from config.

Webhook CRUD Examples

// List all webhooks
$webhooks = Tabby::webhooks()->all();

// Retrieve a webhook
$webhook = Tabby::webhooks()->retrieve($webhookId);

// Update a webhook
$updated = Tabby::webhooks()->update($webhookId, [
    'url' => 'https://example.com/webhooks/tabby-v2',
]);

// Delete a webhook
Tabby::webhooks()->delete($webhookId);

Incoming Webhook Handler Example

Tabby sends payment updates as a POST request with a JSON body. The top-level id field is the payment ID (the same value as payment_id in redirect URLs).

Example payload:

{
  "id": "string",
  "created_at": "2021-09-14T13:08:54Z",
  "expires_at": "2022-09-14T13:08:54Z",
  "closed_at": "2021-09-14T13:09:45Z",
  "status": "closed",
  "is_test": false,
  "is_expired": false,
  "amount": "100",
  "currency": "SAR",
  "order": {
    "reference_id": "string"
  },
  "captures": [],
  "refunds": [],
  "meta": {
    "order_id": null,
    "customer": null
  },
  "token": "string"
}

Laravel controller example — verify the webhook, then retrieve and capture using $request->input('id') as the payment ID:

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use MustafaTaj\Tabby\Facades\Tabby;
use MustafaTaj\Tabby\Webhooks\WebhookPayload;

class TabbyWebhookController
{
    public function __invoke(Request $request): Response
    {
        if (! WebhookPayload::verifyAuthHeader(
            headers: ['X-Webhook-Secret' => (string) $request->header('X-Webhook-Secret')],
            headerName: 'X-Webhook-Secret',
            expectedValue: config('services.tabby.webhook_secret'),
        )) {
            abort(401);
        }

        $payload = WebhookPayload::fromJson($request->getContent());
        $paymentId = $request->input('id'); // same as $payload->paymentId()

        if ($payload->isAuthorizedEvent()) {
            $result = Tabby::payments()->retrieveAndCapture(
                paymentId: $paymentId,
                referenceId: $payload->orderReferenceId(),
            );

            if ($result['successful']) {
                // Fulfill the order in your OMS
            }
        }

        if ($payload->isClosedEvent()) {
            // Payment completed — no capture action required
        }

        return response('OK', 200);
    }
}

Plain PHP example:

use MustafaTaj\Tabby\Tabby;
use MustafaTaj\Tabby\Webhooks\WebhookPayload;

$payload = WebhookPayload::fromJson(file_get_contents('php://input'));
$paymentId = $payload->paymentId(); // reads the "id" field from the webhook body

if ($payload->isAuthorizedEvent()) {
    $tabby = Tabby::make($config);
    $result = $tabby->payments()->retrieveAndCapture(
        paymentId: $paymentId,
        referenceId: $payload->orderReferenceId(),
    );
}

Webhook payloads use lowercase statuses (authorized, closed). The parser normalizes them via PaymentStatus.

Respond with HTTP 200 immediately and process asynchronously when possible. Always verify the final payment state via the Tabby API — do not rely on the webhook body alone as your only source of truth.

Error Handling

use MustafaTaj\Tabby\Exceptions\ApiException;
use MustafaTaj\Tabby\Exceptions\AuthenticationException;
use MustafaTaj\Tabby\Exceptions\ConfigurationException;
use MustafaTaj\Tabby\Exceptions\NetworkException;
use MustafaTaj\Tabby\Exceptions\ValidationException;

try {
    $payment = Tabby::payments()->retrieve($paymentId);
} catch (AuthenticationException $e) {
    // HTTP 401 / 403
} catch (ValidationException $e) {
    // HTTP 400 / 422
} catch (ApiException $e) {
    // Other non-success API responses
    $status = $e->getStatusCode();
    $body = $e->getResponseJson();
} catch (NetworkException $e) {
    // Connection errors and timeouts
} catch (ConfigurationException $e) {
    // Missing or invalid SDK configuration
}

Exception objects expose sanitized request context and never include raw secret keys.

Optional DTOs

All resource methods accept plain arrays. DTOs are optional helpers:

use MustafaTaj\Tabby\DTO\Payment\CapturePaymentData;

Tabby::payments()->captureWithData(
    paymentId: $paymentId,
    data: new CapturePaymentData(
        amount: '100.00',
        referenceId: 'capture-1001',
    ),
);

Custom HTTP Client

Implement MustafaTaj\Tabby\Contracts\HttpClientInterface and pass it to Tabby::make():

$tabby = Tabby::make($config, $customHttpClient);

Testing

composer validate
composer dump-autoload
vendor/bin/phpunit
vendor/bin/phpstan analyse

The test suite uses mocked HTTP clients and does not make real Tabby API calls.

GitHub Actions runs the same checks on PHP 8.2 through 8.4 for every push and pull request to main. The package runtime still supports PHP 8.1+; Laravel dev dependencies require PHP 8.2+.

Security Notes

  • Never commit real Tabby public or secret keys to source control.
  • Use sandbox/test credentials during development (IS_TABBY_SANDBOX=true).
  • Store secrets in .env or a secure secret manager.
  • Do not expose secret keys to frontend clients. Public keys are intended for checkout session creation only.
  • Validate incoming webhook requests in your application according to your security rules and any Tabby-provided headers or secrets.

Contributing

Contributions are welcome. Please open an issue or pull request on GitHub.

License

This package is open-sourced software licensed under the MIT license.