PHPackages                             transistorized-cmd/stripe-toolkit-webhooks - 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. [Queues &amp; Workers](/categories/queues)
4. /
5. transistorized-cmd/stripe-toolkit-webhooks

ActiveLibrary[Queues &amp; Workers](/categories/queues)

transistorized-cmd/stripe-toolkit-webhooks
==========================================

Bulletproof Stripe webhook handling for Laravel — the first module of The Complete Stripe Toolkit. Idempotency, store-then-process, queue dispatch, snapshot + thin events.

v1.0.0(1mo ago)02↓100%MITPHPPHP ^8.2CI passing

Since May 7Pushed 1mo agoCompare

[ Source](https://github.com/transistorized-cmd/stripe-toolkit-webhooks)[ Packagist](https://packagist.org/packages/transistorized-cmd/stripe-toolkit-webhooks)[ Docs](https://github.com/transistorized-cmd/stripe-toolkit-webhooks)[ RSS](/packages/transistorized-cmd-stripe-toolkit-webhooks/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (13)Versions (2)Used By (0)

Bulletproof Stripe Webhooks for Laravel
=======================================

[](#bulletproof-stripe-webhooks-for-laravel)

[![Tests](https://github.com/transistorized-cmd/stripe-toolkit-webhooks/actions/workflows/tests.yml/badge.svg)](https://github.com/transistorized-cmd/stripe-toolkit-webhooks/actions/workflows/tests.yml)[![PHP](https://camo.githubusercontent.com/6bad160453b32dfe4d3de2190f69bf07d369d1d96c3280631a4154dc02d370dc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322d2d382e342d373737424234)](https://www.php.net)[![Laravel](https://camo.githubusercontent.com/75389260b59609d7720f46e2ba37d7c50ba1b0685dab215d4190ea2450fba61b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d31312d2d31332d464632443230)](https://laravel.com)[![Stripe SDK](https://camo.githubusercontent.com/c51a925513da610806d41bd92e94fe1b9216897b8ba963c52d27d4f6b99d2c63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53747269706525323053444b2d31352d2d31372d363335424646)](https://github.com/stripe/stripe-php)[![License: MIT](https://camo.githubusercontent.com/8174925d009b42074d50ab5cc7e29fcb1aa613b0d9cb2e43097697a40cf90fa4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f77)](LICENSE.md)

> First module of **The Complete Stripe Toolkit for Laravel**.
>
> The webhook reliability layer your future-self wishes you had shipped on day one: idempotent, queue-backed, observable — for both classic snapshot webhooks and the new Event Destinations (thin events) — under a single typed DTO.

> **Status:** `v1.0.0-rc.1`. Free core feature-complete, 51 Pest tests green. Distilled from 10+ years of shipping Laravel and Stripe integrations across production SaaS apps. Pro module ships separately.

---

A taste
-------

[](#a-taste)

```
use TransistorizedCmd\StripeToolkit\Webhooks\Attributes\StripeEvent;
use TransistorizedCmd\StripeToolkit\Webhooks\Contracts\WebhookEventDTO;
use TransistorizedCmd\StripeToolkit\Webhooks\StripeWebhookHandler;

#[StripeEvent('payment_intent.succeeded')]
class FulfillOrder extends StripeWebhookHandler
{
    public int $tries = 5;
    public array $backoff = [60, 300, 900, 1800, 3600];

    public function handle(WebhookEventDTO $event): void
    {
        /** @var \Stripe\PaymentIntent $intent */
        $intent = $event->relatedObject();

        Order::where('stripe_pi', $intent->id)
            ->firstOrFail()
            ->markPaid($intent->amount, $intent->currency);
    }
}
```

That's the whole thing. Drop the file under `app/Stripe/Handlers/`, and the kit auto-discovers it via the attribute, persists every incoming webhook keyed by `event.id`, queues your handler with its own retry schedule, and dead-letters cleanly when retries run out.

The same handler matches `payment_intent.succeeded` *and*`v1.payment_intent.succeeded` — when you migrate to Event Destinations, your business code doesn't change.

---

Who this is for
---------------

[](#who-this-is-for)

You'll get the most out of this package if you fit one of these:

- **You run a Laravel SaaS in production** and you've already lived through a webhook outage — silent failures, double-processed events, duplicate emails to customers, accounting that doesn't reconcile. You want a layer that makes the next integration boring.
- **You're rolling out Stripe Connect or Event Destinations** and you need both the legacy snapshot webhooks and the new V2 thin events flowing through the same handlers without forking your code.
- **You're a tech lead at a payments-heavy product** and you'd rather install a vetted package than re-derive idempotency, retry semantics, and dead-letter tracking from first principles for the fourth time.

This is **not** the easiest entry point if you're brand new to Stripe or Laravel queues — the kit assumes you know why `event.id` matters and what backoff means. If that's you, ship `spatie/laravel-stripe-webhooks`first, hit a wall, then come back.

---

What it does
------------

[](#what-it-does)

The pipeline, from POST to handler:

```
Stripe POST /stripe/webhook
    │
    ▼
[Controller]
  1. Verify signature (HMAC SHA256, constant-time, tolerance configurable)
  2. Detect format (snapshot vs thin) by inspecting payload.object
  3. Persist row in stripe_webhook_calls keyed by event.id (UNIQUE)
       └─ if event.id already PROCESSED → 200 "duplicate", no dispatch
       └─ if event.id already RECEIVED  → 200 "in_progress" (race winner owns it)
  4. Fire WebhookReceived event
  5. Dispatch ProcessStripeWebhook to queue
  6. Return 200 (target: handle($event); mark processed; }
  Catch { categorize error; release with backoff; }
  After $tries failures → mark dead_letter, fire WebhookDeadLettered

```

What you get on top of that:

- **Idempotency at the event level** — same `event.id` twice is a no-op for handlers, including under concurrent delivery (race-tested).
- **Per-handler retries** — one slow handler doesn't poison the rest; each is its own job with its own `$tries`/`$backoff`.
- **Native typed Stripe objects** — `relatedObject()` returns `\Stripe\PaymentIntent`, `\Stripe\Charge`, etc., not arrays.
- **Type normalization** — `payment_intent.succeeded` and `v1.payment_intent.succeeded` route to the same handler; the prefix-stripping happens automatically.
- **Multi-secret routing** — `/stripe/webhook/{configKey}` lets you carry multiple Stripe webhook endpoints (Connect, separate mode, etc.) on a single Laravel app.
- **Connect-aware** — `accountId()` on the DTO surfaces the connected account id from the payload (snapshot or thin).
- **Read-only debug inspector** at `/stripe-webhooks-debug` (dev-mode) with a built-in form trigger to send signed test events to your own endpoint without leaving the browser.
- **Three artisan commands**: `install`, `prune`, `migrate-from-spatie`.

---

Why this and not…
-----------------

[](#why-this-and-not)

`spatie/laravel-stripe-webhooks``laravel/cashier`This kitPersists the call before processingyesno**yes**Idempotency on `event.id`partialno**event-level + per-handler**Per-handler retries with own backoffnono**yes**Dead-letter tracking with error categorynono**yes**Adapter for thin events (Event Destinations)nono**yes**Type normalization (snapshot ↔ thin)non/a**yes**`relatedObject()` returns native Stripe typesnon/a**yes**`accountId()` for Connectnopartial**yes**Read-only debug UInono**yes**Dependency on subscription modelnone**required**noneScopewebhook plumbingsubscription billingwebhook plumbing + observability`spatie/laravel-stripe-webhooks` is the dominant package by downloads (3M+) — it works, it's stable, it persists. The kit is what you reach for when you outgrow it: when you want per-handler reliability and observability, when you need thin events, when you're tired of forking your handler logic by event-type version. There's a [migration command](#artisan-commands) to import your existing Spatie history.

---

Install
-------

[](#install)

```
composer require transistorized-cmd/stripe-toolkit-webhooks
php artisan stripe-webhooks:install
php artisan migrate
```

`stripe-webhooks:install` does three things:

1. Publishes `config/stripe-webhooks.php`
2. Publishes the two migrations (`stripe_webhook_calls` and `stripe_webhook_handler_runs`)
3. Scaffolds a sample handler at `app/Stripe/Handlers/HandlePaymentIntentSucceeded.php` so you have something to edit immediately

Add the signing secret to `.env`:

```
STRIPE_WEBHOOK_SECRET=whsec_…

```

Register the route:

```
// routes/web.php
use TransistorizedCmd\StripeToolkit\Webhooks\Facades\StripeWebhook;

StripeWebhook::route('stripe/webhook');
```

If your route lives in `routes/web.php` (rather than `routes/api.php`), exclude it from CSRF in `bootstrap/app.php`:

```
->withMiddleware(function (Middleware $middleware): void {
    $middleware->validateCsrfTokens(except: [
        'stripe/webhook',
        'stripe/webhook/*',
    ]);
})
```

Run a queue worker:

```
php artisan queue:work --queue=stripe-webhooks
```

Done. Your endpoint is live.

### Choosing a Stripe API version

[](#choosing-a-stripe-api-version)

When you register the endpoint in **Stripe Dashboard → Developers → Webhooks** (or via `stripe listen`), Stripe asks you to pick an API version. The choice affects the **shape of the payloads** Stripe sends to this endpoint — Stripe locks the version per endpoint, so it becomes a one-way migration switch independent of your account default.

OptionUse it when**Latest stable** (e.g. `2026-04-22.dahlia` at time of writing)New endpoints / new accounts. Aligns the payload shape with the most recent SDK and gets you all the modern fields.**Your account's current version** (e.g. `2023-10-16`)You're integrating into an existing app pinned to that version. Match it so your endpoint's payloads match what `Stripe::PaymentIntent::retrieve()` returns elsewhere in your code.**`.preview`** versionsAvoid for production. They include features in evolution; the SDK may not deserialize newly-added fields cleanly.The kit itself is version-agnostic — it persists `api_version` on every `stripe_webhook_calls` row for auditing but doesn't branch on it. Your handlers receive typed `\Stripe\…` objects either way; field availability follows whatever version the endpoint is locked to.

> **Note:** Stripe's "locked per endpoint" semantics are useful for gradual migrations. You can pin one endpoint to `dahlia` to start receiving new fields, while the rest of your account stays on `2023-10-16`. When you're ready, upgrade your account default to match.

---

Your first handler
------------------

[](#your-first-handler)

Two ways to register, both work, you can mix them.

**Attribute discovery** — recommended, autoloaded from `app/Stripe/Handlers/` (path is configurable):

```
#[StripeEvent('invoice.payment_failed')]
#[StripeEvent('invoice.payment_action_required')]   // attribute is repeatable
class StartDunning extends StripeWebhookHandler
{
    public function handle(WebhookEventDTO $event): void
    {
        /** @var \Stripe\Invoice $invoice */
        $invoice = $event->relatedObject();
        // …
    }
}
```

**Config map** — explicit, useful when registering closures or deferring class loading:

```
// config/stripe-webhooks.php
'handlers' => [
    'invoice.payment_failed' => [
        \App\Stripe\Handlers\NotifyCustomer::class,
        \App\Stripe\Handlers\StartDunning::class,
    ],
],
```

Whichever you pick, the DTO contract is identical:

MethodReturns`id()``string` — Stripe `evt_…``type()``string` — raw type, e.g. `payment_intent.succeeded``normalizedType()``string` — version-prefix-stripped`createdAt()``\DateTimeImmutable``apiVersion()``?string` — present on snapshot, null on thin`accountId()``?string` — Connected Account id, when applicable`livemode()``bool``sourceFormat()``EventSource::Snapshot | EventSource::Thin``relatedObject()``mixed` — typed `\Stripe\…` instance (lazy on thin)`rawPayload()``string` — what Stripe POSTed, byte-for-byteSee [`examples/`](examples) for four production-shaped handlers covering fulfillment, dunning, refund reconciliation, and access revocation.

---

Multi-secret routing &amp; Stripe Connect
-----------------------------------------

[](#multi-secret-routing--stripe-connect)

Two complementary mechanisms for Connect:

### Per-endpoint secrets

[](#per-endpoint-secrets)

```
// routes/web.php
StripeWebhook::route('stripe/webhook/{configKey}');
```

```
// config/stripe-webhooks.php
'webhook_secrets' => [
    'default'        => env('STRIPE_WEBHOOK_SECRET'),
    'platform'       => env('STRIPE_WEBHOOK_SECRET_PLATFORM'),
    'connect_v2'     => env('STRIPE_WEBHOOK_SECRET_CONNECT_V2'),
],
```

A POST to `/stripe/webhook/platform` verifies against the `platform`secret. The `configKey` is recorded on every `WebhookCall`, so you can filter the inspector or query history by source.

### `accountId()` on the DTO

[](#accountid-on-the-dto)

When a single endpoint receives events from many connected accounts (typical Connect setup), the kit extracts the account id and exposes it on the DTO:

```
public function handle(WebhookEventDTO $event): void
{
    if ($accountId = $event->accountId()) {
        $tenant = Tenant::where('stripe_account_id', $accountId)->firstOrFail();
        // proceed with tenant context
    }
}
```

The two are not alternatives — use both: the route param identifies the secret for HMAC verification, the accountId identifies the tenant.

---

Snapshot + thin events
----------------------

[](#snapshot--thin-events)

Stripe sends two formats:

- **Snapshot** (`/v1/webhooks` legacy): full `event.data.object` in the payload.
- **Thin** (Event Destinations / V2, GA Oct 2024): minimal envelope with `related_object` metadata only — the actual resource is fetched on demand.

You don't have to think about this. The kit:

1. Detects format from `payload.object` (`event` vs `v2.core.event`) before signature verification.
2. Verifies with the right code path (`Stripe\Webhook::constructEvent`vs `Stripe\WebhookSignature::verifyHeader`).
3. Hydrates a typed wrapper either way (`SnapshotEventDTO` or `ThinEventDTO`).
4. Returns the same contract from `relatedObject()` — typed Stripe objects, instant for snapshot, lazy-fetched + cached for thin.

Type normalization means handlers registered as `payment_intent.succeeded`match `v1.payment_intent.succeeded` automatically. Migrate to Event Destinations whenever Stripe forces you; your code doesn't change.

`stripe/stripe-php` `^15`/`^16` are supported, but thin event support needs `^17`. If a v2 payload arrives on an older SDK, you get a clean 500 with an explicit `UnsupportedSdkVersionException` rather than silent failure.

---

Reconciling with Stripe when webhooks fail
------------------------------------------

[](#reconciling-with-stripe-when-webhooks-fail)

Webhooks are best-effort: networks blip, deploys race, signing secrets drift, and Stripe eventually gives up retrying. When state has diverged, ask the API directly — Stripe is always the source of truth.

The kit ships a small `StripeReconciler` for this. Two patterns:

### Pattern A — app reconcile (the demo uses this)

[](#pattern-a--app-reconcile-the-demo-uses-this)

You have a stored Stripe id (`stripe_checkout_session_id`, `stripe_payment_intent_id`, …) on your own model. Fetch the live state and apply your business logic:

```
use TransistorizedCmd\StripeToolkit\Webhooks\Support\StripeReconciler;

public function reconcile(Order $order, StripeReconciler $reconciler): RedirectResponse
{
    /** @var \Stripe\Checkout\Session $session */
    $session = $reconciler->fetchObject($order->stripe_checkout_session_id);

    if ($session->payment_status === 'paid') {
        $order->markPaid(/* … */);
    }

    return back();
}
```

`fetchObject($id)` routes by id prefix (`pi_`, `ch_`, `cs_`, `cus_`, `sub_`, `in_`, `evt_`) and returns the typed `\Stripe\…` instance. Implements the kit's `Contracts\StripeObjectFetcher` interface — bind your own implementation if you need extra prefixes or different SDK options (Stripe Connect's `stripe_account` header, etc.).

### Pattern B — re-run handlers against fresh state

[](#pattern-b--re-run-handlers-against-fresh-state)

You have a stored `WebhookCall` row that's stuck — the call landed but processing failed, or the handler was deployed broken and you've since fixed it. Tell the kit to refetch the related object and re-run the event's handlers:

```
$reconciler->reconcile($webhookCall);
```

The kit synthesises a snapshot DTO carrying the **fresh** related object and dispatches each handler synchronously. Handlers must be idempotent (the kit's docs already say so) — re-running on already-reconciled state is a no-op. A `WebhookReconciled` event is fired so you can record audit trails.

### Pro tier — operator tooling

[](#pro-tier--operator-tooling)

The free core ships the primitive. The Pro module wraps it in operator-grade tooling:

- `php artisan stripe-webhooks:reconcile {id|--pending|--older=10m}`for batch recovery
- A "Reconcile" action on the Filament `WebhookCallResource`
- Audit log of who ran what reconcile and when
- Throttling and back-pressure for large batches against Stripe rate limits

For local recovery and one-off operator workflows, the free primitive is enough. Reach for Pro when you need the dashboard.

---

Debug inspector
---------------

[](#debug-inspector)

In `local`/`testing` environments the kit exposes a read-only Blade UI at `/stripe-webhooks-debug`:

- Live-updating table of recent calls with status counters and filters
- Detail view per call with metadata, handler runs, error stack traces, and pretty-printed payload
- Embedded form trigger that signs payloads server-side and POSTs them to your own endpoint — useful for replicating Stripe's exact wire format during development
- "Duplicate this event" links from rows and detail page

Auto-disabled in `production` unless you explicitly opt in:

```
STRIPE_WEBHOOKS_DEBUG=true
STRIPE_WEBHOOKS_DEBUG_PATH=/internal/webhooks   # optional: change the URL

```

The form trigger makes the app POST to itself. PHP's built-in dev server is single-threaded by default; run it with workers:

```
PHP_CLI_SERVER_WORKERS=4 php artisan serve --no-reload
```

---

Testing helpers
---------------

[](#testing-helpers)

Sign payloads programmatically in your test suite:

```
use TransistorizedCmd\StripeToolkit\Webhooks\Tests\Support\SignedPayload;

$body = SignedPayload::body($eventArray);
$header = SignedPayload::header($body, 'whsec_test_default');

$this->call('POST', '/stripe/webhook', [], [], [], [
    'HTTP_STRIPE_SIGNATURE' => $header,
    'CONTENT_TYPE' => 'application/json',
], $body)->assertOk();
```

Pre-built fixtures cover snapshot (with optional Connect account) and thin event payloads:

```
use TransistorizedCmd\StripeToolkit\Webhooks\Tests\Fixtures\Fixtures;

$payload = Fixtures::snapshotPaymentIntentSucceeded(accountId: 'acct_…');
$thinPayload = Fixtures::thinV1CustomerCreated();
```

A richer `WebhookFactory` API — fluent builders for every event type, à la `stripe trigger` but Laravel-native — ships in the Pro module.

---

Laravel events to hook
----------------------

[](#laravel-events-to-hook)

```
use TransistorizedCmd\StripeToolkit\Webhooks\Events\WebhookDeadLettered;
use TransistorizedCmd\StripeToolkit\Webhooks\Events\WebhookHandlerFailed;
use TransistorizedCmd\StripeToolkit\Webhooks\Events\WebhookProcessed;
use TransistorizedCmd\StripeToolkit\Webhooks\Events\WebhookReceived;

Event::listen(WebhookDeadLettered::class, function (WebhookDeadLettered $e) {
    Slack::send("Stripe webhook DLQ: {$e->webhookCall->stripe_event_id}");
});
```

EventFires when`WebhookReceived`Persisted to DB, before queue dispatch`WebhookProcessed`All handlers for an event finished OK`WebhookHandlerFailed`A handler attempt failed (may still retry)`WebhookDeadLettered`A handler exhausted retries — DLQ entry---

Artisan commands
----------------

[](#artisan-commands)

CommandWhat it does`stripe-webhooks:install`Publishes config + migrations + sample handler stub`stripe-webhooks:prune`Deletes rows past the retention horizon (`--dry-run`, `--status=` filters)`stripe-webhooks:migrate-from-spatie`Imports history from a `spatie/laravel-stripe-webhooks` install (`--dry-run`, `--source-table=`, `--source-name=`, `--batch-size=`, `--since=`)Schedule the prune in `routes/console.php`:

```
Schedule::command('stripe-webhooks:prune')->dailyAt('03:00');
```

Retention is configurable per status (see below).

---

Configuration reference
-----------------------

[](#configuration-reference)

`config/stripe-webhooks.php` after `stripe-webhooks:install`:

```
return [
    'webhook_secrets' => [
        'default' => env('STRIPE_WEBHOOK_SECRET'),
    ],

    'route' => [
        'path' => 'stripe/webhook',
        'middleware' => ['api'],
    ],

    'queue' => [
        'connection' => env('STRIPE_QUEUE_CONNECTION', env('QUEUE_CONNECTION', 'sync')),
        'name' => env('STRIPE_QUEUE_NAME', 'stripe-webhooks'),
    ],

    'tables' => [
        'webhook_calls' => 'stripe_webhook_calls',
        'handler_runs' => 'stripe_webhook_handler_runs',
    ],

    'retention' => [
        'processed_days' => 90,         // null = retain forever
        'failed_days' => 365,
        'dead_letter_days' => null,     // keep DLQ forever by default
    ],

    'tolerance' => 300,                  // signature timestamp tolerance (s)

    'handlers' => [
        // 'invoice.payment_failed' => [\App\Stripe\Handlers\StartDunning::class],
    ],

    'discover_attributes' => true,
    'discover_path' => null,             // null → app_path('Stripe/Handlers')

    'debug' => [
        'enabled' => env('STRIPE_WEBHOOKS_DEBUG', null),  // null = auto (on outside production)
        'path' => env('STRIPE_WEBHOOKS_DEBUG_PATH', 'stripe-webhooks-debug'),
        'middleware' => ['web'],
        'per_page' => 25,
        'auto_refresh_seconds' => 5,
    ],
];
```

---

Compatibility matrix
--------------------

[](#compatibility-matrix)

CI runs three combinations:

PHPLaravelStripe SDK8.211.x^15.08.312.x^16.08.413.x^17.0Plus a static-analysis job (Pint preset Laravel + PHPStan / Larastan level 5) that runs before the matrix.

Filament v4 is an optional `suggest` for the Pro module's admin panel. The free core doesn't depend on it.

---

Documentation
-------------

[](#documentation)

This README is the executive summary. The full guide lives in [`docs/`](docs):

- [Installation](docs/installation.md)
- [Writing handlers](docs/handlers.md)
- [Multi-secret · Connect](docs/multi-secret-connect.md)
- [Thin events](docs/thin-events.md)
- [Debug inspector](docs/debug-inspector.md)
- [Migrating from Spatie](docs/migrating-from-spatie.md)
- [Troubleshooting](docs/troubleshooting.md)
- [FAQ](docs/faq.md)

The site is VitePress; `cd docs && npm install && npm run docs:dev` to preview locally.

---

Roadmap &amp; Pro module
------------------------

[](#roadmap--pro-module)

This module focuses on the **infrastructure** of Stripe webhooks. The companion Pro module (`transistorized-cmd/stripe-toolkit-webhooks-pro`) adds the operator-facing parts:

- Filament v4 admin panel: replay actions, DLQ inspector, batch operations, advanced filters
- `stripe-webhooks:replay` CLI command
- DLQ alert notifications (Slack, email, custom channels)
- Test factories — Laravel-native equivalent to `stripe trigger`
- Zero-downtime signing-secret rotation helper
- Pulse + Telescope cards

Pro is paid (Lemon Squeezy). The free core works fully without it.

The wider picture — refunds, disputes, Connect platform mechanics, dunning flows, reconciliation, audit trails — is the subject of an upcoming book in *The Complete Stripe Toolkit for Laravel* series. [Drop your email](#) to be notified when it's ready.

---

Contributing
------------

[](#contributing)

Issues and pull requests welcome. Before opening a PR:

```
composer install
vendor/bin/pint --test         # code style
vendor/bin/phpstan analyse     # static analysis
vendor/bin/pest                # 51 tests, ~0.8s
```

Security issues: please email rather than file publicly.

---

Credits
-------

[](#credits)

- Built by [Jose Luis Pellicer](https://github.com/transistorized-cmd)([transistorized-cmd](https://github.com/transistorized-cmd)) — 10+ years building Laravel applications, with deep Stripe integration work across multiple production SaaS products.
- Inspired by the gaps in `spatie/laravel-stripe-webhooks` — which remains a great default for simpler use cases.

License
-------

[](#license)

MIT — see [LICENSE.md](LICENSE.md).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance93

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

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

Unknown

Total

1

Last Release

33d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/180107401?v=4)[transistorized-cmd](/maintainers/transistorized-cmd)[@transistorized-cmd](https://github.com/transistorized-cmd)

---

Top Contributors

[![jlpellicer](https://avatars.githubusercontent.com/u/2797529?v=4)](https://github.com/jlpellicer "jlpellicer (16 commits)")

---

Tags

laravelstripequeuewebhooksidempotencystripe-toolkitthin-eventsevent-destinations

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/transistorized-cmd-stripe-toolkit-webhooks/health.svg)

```
[![Health](https://phpackages.com/badges/transistorized-cmd-stripe-toolkit-webhooks/health.svg)](https://phpackages.com/packages/transistorized-cmd-stripe-toolkit-webhooks)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[laravel/cashier

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

2.5k28.4M134](/packages/laravel-cashier)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k14.1M120](/packages/laravel-pulse)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9732.3M121](/packages/roots-acorn)[spatie/laravel-health

Monitor the health of a Laravel application

88011.3M149](/packages/spatie-laravel-health)[api-platform/laravel

API Platform support for Laravel

59156.3k10](/packages/api-platform-laravel)

PHPackages © 2026

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