PHPackages                             syriable/laravel-payments - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. syriable/laravel-payments

ActiveLibrary[Payment Processing](/categories/payments)

syriable/laravel-payments
=========================

A lightweight Laravel package for accepting payments through Stripe, PayPal, and an open ecosystem of community gateway plugins.

v1.0.0(2mo ago)12MITPHP ^8.3

Since May 20Compare

[ Source](https://github.com/syriable/laravel-payments)[ Packagist](https://packagist.org/packages/syriable/laravel-payments)[ Docs](https://github.com/syriable/laravel-payments)[ GitHub Sponsors](https://github.com/laravel-payments)[ RSS](/packages/syriable-laravel-payments/feed)WikiDiscussions Synced 3w ago

READMEChangelog (3)Dependencies (12)Versions (6)Used By (0)

    ![Syriable Payments](art/logo-light.svg)

 [![Latest Version on Packagist](https://camo.githubusercontent.com/3fdaff0e50a07db8203bc311a64ce67da9f9048f79d3dfe6feacd26f854ff4d9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7379726961626c652f6c61726176656c2d7061796d656e74732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/syriable/laravel-payments) [![Tests](https://camo.githubusercontent.com/7cfd633b593a43a46eb471a383e32e819a88d7d2dc70eccd6dfa314ad0648ea5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7379726961626c652f6c61726176656c2d7061796d656e74732f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/syriable/laravel-payments/actions/workflows/run-tests.yml) [![Total Downloads](https://camo.githubusercontent.com/e51ad9bb0f92341619883a3a3f716c4fb698046c116e1afbedf27fd6fe39c619/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7379726961626c652f6c61726176656c2d7061796d656e74732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/syriable/laravel-payments) [![PHP Version](https://camo.githubusercontent.com/ff70bd011bce39647bdb9bb155865ee0b5cb2d7159e7b67dc4f9702c2ccc2dd5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f7379726961626c652f6c61726176656c2d7061796d656e74732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/syriable/laravel-payments) [![License](https://camo.githubusercontent.com/8952af55508e7f2acb2777ee42c30cf3ddb7a75939c912d5b37be1acdae4066e/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f7379726961626c652f6c61726176656c2d7061796d656e74733f7374796c653d666c61742d737175617265)](https://github.com/syriable/laravel-payments/blob/main/LICENSE.md)

Laravel Payments
================

[](#laravel-payments)

A lightweight Laravel package for accepting payments through **Stripe**, **PayPal**, and an open ecosystem of community gateway plugins.

It does one thing well: it unifies payment gateways behind a small, Laravel-native API. It is **not** an accounting system, a subscription engine, or a billing platform. It ships a single table to make webhook delivery durable, and otherwise stays out of your schema — it never models your domain for you.

```
use Syriable\Payments\Data\Checkout;
use Syriable\Payments\Facades\Gateway;

$result = Gateway::driver('stripe')->checkout(new Checkout(
    amount: 2500,            // minor units — 2500 == $25.00
    currency: 'USD',
    reference: "order_{$order->id}",
    successUrl: route('orders.success', $order),
    cancelUrl: route('orders.cancel', $order),
));

return redirect($result->redirectUrl);
```

Why this package
----------------

[](#why-this-package)

- **Tiny core.** A manager, a contract, three DTOs, two gateways. You can read the whole thing in an afternoon.
- **Laravel-native.** Built on `Illuminate\Support\Manager` — the same pattern behind `Cache`, `Mail`, and `Storage`. Nothing new to learn.
- **Financially safe by design.** Amounts are integer minor units. No floating-point math touches money, anywhere.
- **No hard SDK dependencies.** Gateways use Laravel's HTTP client. Your `vendor/` directory stays lean.
- **Plugin-friendly.** Add a gateway in ~20 lines, in your own Composer package, shipped on your own schedule.

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 12 or 13

Installation
------------

[](#installation)

```
composer require syriable/laravel-payments
```

The service provider and the `Gateway` facade are auto-discovered. Publish the config:

```
php artisan vendor:publish --tag="laravel-payments-config"
```

Verified webhooks are persisted before processing (see [Webhooks](#webhooks)), so publish and run the migration:

```
php artisan vendor:publish --tag="laravel-payments-migrations"
php artisan migrate
```

Prefer not to store webhooks? Set `webhook.store` to `Syriable\Payments\Store\NullWebhookStore::class` and skip the migration.

Then set your credentials in `.env`:

```
PAYMENT_GATEWAY=stripe

STRIPE_SECRET=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx

PAYPAL_MODE=live
PAYPAL_CLIENT_ID=xxx
PAYPAL_CLIENT_SECRET=xxx
PAYPAL_WEBHOOK_ID=xxx
```

`STRIPE_WEBHOOK_SECRET` and `PAYPAL_WEBHOOK_ID` come from each provider's dashboard when you register your webhook endpoint (see [Webhooks](#webhooks)). Until they are set, incoming webhooks fail signature verification and return `403` — so configure them before going live.

Usage
-----

[](#usage)

### Checkout

[](#checkout)

```
use Syriable\Payments\Data\Checkout;
use Syriable\Payments\Facades\Gateway;

$checkout = new Checkout(
    amount: 2500,
    currency: 'USD',
    reference: 'order_42',
    successUrl: 'https://shop.test/success',
    cancelUrl: 'https://shop.test/cancel',
    customerEmail: 'buyer@example.com',   // optional
    metadata: ['order_id' => 42],          // optional
);

// Explicit gateway:
$result = Gateway::driver('stripe')->checkout($checkout);

// Or the default gateway from config — __call passthrough handles this:
$result = Gateway::checkout($checkout);

return redirect($result->redirectUrl);
```

`checkout()` returns a `PaymentResult`:

```
$result->id;            // gateway's payment/session id
$result->status;        // PaymentStatus enum (Pending, RequiresAction, Processing, Paid, Failed, Canceled, PartiallyRefunded, Refunded)
$result->redirectUrl;   // hosted-checkout URL, if any
$result->reference;     // your checkout reference, when the gateway echoes it (e.g. on retrieve())
$result->amount;        // gateway-reported amount in minor units, when available
$result->currency;      // gateway-reported ISO 4217 currency, when available
$result->raw;           // full untouched gateway response
```

> **Amounts are always integer minor units.** `2500` means `$25.00`, `100` means `$1.00`. Never pass a float or a major-unit value like `25.00` — `Checkout` rejects non-positive amounts at construction, but it cannot tell `25` cents from `25` dollars. Keeping money as integers is what makes the package financially safe; no floating-point math touches an amount anywhere.

> **Store `$result->id` on your order.** This is the gateway's identifier for the payment, and it is how the webhook later reconciles back to your order (see [Webhooks](#webhooks)). A typical checkout persists it immediately:
>
> ```
> $order->update([
>     'gateway'            => 'stripe',
>     'gateway_payment_id' => $result->id,
> ]);
> ```

### Reconciliation

[](#reconciliation)

Webhooks can be lost (downtime, deploys, secret rotation). `retrieve()` pulls the authoritative state straight from the gateway, so you can reconcile orders stuck in a non-final state:

```
$result = Gateway::driver('stripe')->retrieve($order->gateway_payment_id);

if ($result->status === PaymentStatus::Paid) {
    $order->markPaid();
}
```

`retrieve()` also returns `$reference`, `$amount`, and `$currency` when the gateway exposes them, so you can verify a reconciled payment exactly as you would a webhook.

For the common case — re-emit the canonical event so your existing webhook listeners fire — use the `ReconcilePayment` job or the command:

```
php artisan payment:reconcile stripe pi_xxx          # queued (default)
php artisan payment:reconcile stripe pi_xxx --sync   # inline
```

```
use Syriable\Payments\Jobs\ReconcilePayment;

ReconcilePayment::dispatch('stripe', $order->gateway_payment_id);
```

The package doesn't own your orders table, so iterate your own non-final orders on a schedule and reconcile each. Terminal states re-dispatch `PaymentSucceeded` / `PaymentFailed` / `PaymentRefunded`; non-terminal states emit nothing.

### Refunds

[](#refunds)

Refunds are an opt-in capability. Check for it with `instanceof` — the type system tells you whether a gateway supports it:

```
use Syriable\Payments\Contracts\Refundable;

$gateway = Gateway::driver('stripe');

if ($gateway instanceof Refundable) {
    $gateway->refund($paymentId);          // full refund
    $gateway->refund($paymentId, 1000);    // partial — 1000 minor units
}
```

### Webhooks

[](#webhooks)

The package registers one webhook route automatically:

```
POST /payment-gateways/webhook/{gateway}

```

Register that URL in each provider's dashboard. The controller verifies the request's signature, then dispatches one of three events. You listen for them in your own application:

```
use Syriable\Payments\Events\PaymentSucceeded;

Event::listen(PaymentSucceeded::class, function (PaymentSucceeded $event) {
    // $event->event is a normalized WebhookEvent. Reconcile on ->reference
    // (your own checkout reference, echoed back by the gateway) rather than
    // ->paymentId: the gateway id can point at different objects across
    // events (a Stripe session id vs. a payment intent id).
    $order = Order::where('reference', $event->event->reference)->first();

    // Always verify the amount before fulfilling.
    if ($order && $event->event->amount === $order->total_minor
        && $event->event->currency === $order->currency) {
        $order->markPaid();
    }
});
```

Events: `PaymentSucceeded`, `PaymentFailed`, `PaymentRefunded`. Each carries a normalized `WebhookEvent` with `$gateway`, `$type`, `$paymentId`, `$reference`, `$amount`, `$currency`, `$eventId`, and the full verified `$payload`.

The controller verifies the signature, persists the event, drops duplicates, and acknowledges immediately; the events are dispatched from a queued `ProcessWebhookEvent` job so a slow listener can't make the gateway time out and retry. Point it at a real queue with `webhook.connection` / `webhook.queue` (defaults to the application's queue).

Verified webhooks are persisted to the `payment_webhook_calls` table before processing (status `pending` → `processed`/`failed`), giving you a durable, auditable record and a place to replay from. Persistence is swappable via the `webhook.store` config key — the default is `Store\DatabaseWebhookStore`; ship your own `Contracts\WebhookStore`, or use `Store\NullWebhookStore` to disable it.

Invalid signatures return `403` and dispatch nothing. Unknown gateways return `404`.

> **Make your listener idempotent.** Gateways legitimately deliver the same webhook more than once. Guard against double-processing — e.g. skip the handler if the order is already marked paid.

> **Keep the webhook route off the `web` middleware group.** `web` enables CSRF protection, which rejects server-to-server webhook requests. The default config uses `api`; change `webhook.middleware` only to something equally CSRF-free.

Observability
-------------

[](#observability)

The package logs the money-movement boundaries — `payments.checkout.created`, `payments.refund.issued`, `payments.webhook.received`, `payments.webhook.duplicate`, and `payments.webhook.invalid_signature` — with ids, references, and amounts (never secrets or full payloads). Route them to their own channel:

```
PAYMENT_LOG_CHANNEL=payments
```

Leave it unset to use the application's default channel.

Adding a custom gateway
-----------------------

[](#adding-a-custom-gateway)

Two ways. For a one-off, register it in `AppServiceProvider::boot()`:

```
use Syriable\Payments\Facades\Gateway;

Gateway::extend('paymob', fn ($app) => new \App\Payments\PaymobGateway(
    config('payment-gateways.gateways.paymob')
));
```

For something reusable, ship it as its own Composer package. A gateway plugin is just a package whose service provider calls `Gateway::extend()`:

```
namespace Vendor\LaravelPaymentsPaymob;

use Illuminate\Support\ServiceProvider;
use Syriable\Payments\Facades\Gateway;

class PaymobServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Gateway::extend('paymob', fn ($app) => new PaymobGateway(
            config('payment-gateways.gateways.paymob', [])
        ));
    }
}
```

Your gateway class implements the `Gateway` contract — and, optionally, `Refundable`:

```
use Syriable\Payments\Contracts\Gateway;
use Syriable\Payments\Contracts\Refundable;

final class PaymobGateway implements Gateway, Refundable
{
    public function name(): string { /* ... */ }
    public function checkout(Checkout $checkout): PaymentResult { /* ... */ }
    public function retrieve(string $paymentId): PaymentResult { /* ... */ }
    public function webhook(Request $request): WebhookEvent { /* ... */ }
    public function refund(string $paymentId, ?int $amount = null): PaymentResult { /* ... */ }
}
```

That's the entire plugin API. No plugin interface, no registry, no manifest.

Testing
-------

[](#testing)

Swap in a fake gateway with one call. No HTTP, no real charges:

```
use Syriable\Payments\Facades\Gateway;

it('checks the customer out', function () {
    $fake = Gateway::fake();

    $this->post('/checkout', ['order' => 42]);

    $fake->assertCheckedOut(fn ($checkout) => $checkout->amount === 2500);
});
```

Available assertions: `assertCheckedOut()`, `assertRefunded()`, `assertNothingCharged()`, `assertCheckoutCount()`.

Configuration
-------------

[](#configuration)

The published `config/payment-gateways.php` is intentionally small:

```
return [
    'default' => env('PAYMENT_GATEWAY', 'stripe'),

    'webhook' => [
        'enabled'    => true,
        'prefix'     => 'payment-gateways',
        'middleware' => ['api'],
        'store'      => Syriable\Payments\Store\DatabaseWebhookStore::class,
    ],

    'gateways' => [
        'stripe' => [
            'secret'         => env('STRIPE_SECRET'),
            'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
        ],
        'paypal' => [
            'mode'          => env('PAYPAL_MODE', 'sandbox'),
            'client_id'     => env('PAYPAL_CLIENT_ID'),
            'client_secret' => env('PAYPAL_CLIENT_SECRET'),
            'webhook_id'    => env('PAYPAL_WEBHOOK_ID'),
        ],
    ],
];
```

Architecture at a glance
------------------------

[](#architecture-at-a-glance)

```
src/
├── Console/        ReconcilePaymentCommand
├── Contracts/      Gateway, Refundable, WebhookStore
├── Data/           Checkout, PaymentResult, WebhookEvent  (readonly DTOs)
├── Enums/          PaymentStatus, WebhookEventType, WebhookCallStatus
├── Events/         PaymentSucceeded, PaymentFailed, PaymentRefunded
├── Exceptions/     PaymentException + 4 specific subclasses
├── Gateways/
│   ├── Concerns/   ResilientHttp
│   ├── Stripe/     StripeGateway
│   └── PayPal/     PayPalGateway
├── Http/           WebhookController
├── Jobs/           ProcessWebhookEvent, ReconcilePayment
├── Models/         WebhookCall
├── Store/          DatabaseWebhookStore, NullWebhookStore
├── Support/        PaymentLog
├── Testing/        FakeGateway, FakeGatewayManager
├── Facades/        Gateway
├── GatewayManager.php
└── PaymentsServiceProvider.php

```

Running the package test suite
------------------------------

[](#running-the-package-test-suite)

```
composer test
```

Static analysis and code style:

```
composer analyse   # PHPStan / Larastan
composer format    # Laravel Pint
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG.md](CHANGELOG.md) for details on what has changed recently.

Security
--------

[](#security)

If you discover a security vulnerability, please email ****rather than using the issue tracker.

Webhook handlers verify signatures before parsing — Stripe via HMAC-SHA256, PayPal via its verification API. A failed verification returns `403` and dispatches no events.

Credits
-------

[](#credits)

- [Syriable](https://github.com/syriable)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). See [LICENSE.md](LICENSE.md).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance87

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

3

Last Release

63d ago

Major Versions

v0.1.1 → v1.0.02026-05-22

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/286110444?v=4)[syriable](/maintainers/syriable)[@syriable](https://github.com/syriable)

---

Tags

laravelstripewebhookspaymentspaypalcheckoutpayment gatewaysyriable

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/syriable-laravel-payments/health.svg)

```
[![Health](https://phpackages.com/badges/syriable-laravel-payments/health.svg)](https://phpackages.com/packages/syriable-laravel-payments)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k30.2M151](/packages/laravel-cashier)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M246](/packages/laravel-ai)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5022.6k](/packages/simplestats-io-laravel-client)[masterix21/laravel-licensing

Laravel licensing package with polymorphic assignment to any model, activation keys, expirations/renewals, and seat control via LicenseUsage. Supports offline verification with public-key–signed tokens, a CLI to generate/rotate/revoke keys, and an extensible architecture via config and contracts.

1613.3k4](/packages/masterix21-laravel-licensing)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
