spatie/laravel-stripe-webhooks
Handle Stripe webhooks in Laravel with automatic signature verification, database logging of valid calls, and dispatch of jobs or events for specific Stripe events. You still implement the payment-related business logic yourself.
Architecture fit. The package is a thin, opinionated adapter: it registers a POST route via Route::stripeWebhooks(), verifies the Stripe signature, persists every valid request to a webhook_calls table, and dispatches either a configured job or a stripe-webhooks::<event> event. It explicitly does not implement payment business logic. That is the correct boundary for a Laravel application — the package owns transport concerns (signature verification, idempotency, durable logging, replay) and leaves domain workflow to application code. The underlying engine is spatie/laravel-webhook-client, which means the package inherits that library's config surface, models, and migration; treat the two as a single coupled dependency in any architecture review.
Integration feasibility. Low effort. Install, publish config, publish the webhook-client migration (note: a different service provider than the package itself), migrate, register the route, and add the route to the CSRF except list. A competent Laravel developer reaches a working endpoint in under a day. The real work — and the real estimate — is the per-event job handlers, which the README correctly flags as out of scope.
Technical risk.
spatie/laravel-webhook-client is the dominant upgrade risk; a major bump there can cascade.default_job ships as an empty string, meaning unmatched events are persisted and never processed. Without an explicit decision, unhandled event types accumulate as dead rows with no alerting.verify_signature can be disabled via env. This must be locked down in staging/production through config review, not convention.WebhookProfile. Confirm the default profile's behavior against your idempotency requirements rather than trusting the prose.Key questions for the vendor/team.
stripe-php and laravel-webhook-client versions are pinned at install time?StripeWebhookProfile dedup key, and is it sufficient for our replay requirements?configKey route parameter and per-secret config keys)?webhook_calls table, and who owns the pruning job?Stack fit. Native fit for any Laravel application already in the Spatie ecosystem, and a reasonable companion to (or replacement for parts of) Laravel Cashier. Two Laravel-version nuances to plan around: on Laravel 11+, CSRF exclusions live in bootstrap/app.php rather than a VerifyCsrfToken $except array, so the README's instruction is stale for modern skeletons; and queue configuration should be set explicitly via STRIPE_WEBHOOK_CONNECTION / STRIPE_WEBHOOK_QUEUE rather than relying on the default queue, so webhook processing cannot be starved by unrelated jobs.
Migration path. If a hand-rolled Stripe controller already exists, do not cut over in one step. Register the new route at a parallel URL, point a test-mode Stripe endpoint at it, and run both paths against mirrored test events until the new handlers produce equivalent state changes. Only then repoint the production endpoint in the Stripe dashboard. Because the package persists raw payloads to webhook_calls, the old and new paths can be compared row-for-row during the shadow period.
Compatibility. Verify three axes before committing: Laravel/PHP version support, stripe-php version alignment with the API version configured in the Stripe dashboard, and laravel-webhook-client major version. Pin the package to a caret range and record the resolved versions in the lockfile review.
Sequencing.
webhook_calls.payment_intent.succeeded, invoice.payment_failed, charge.refunded, customer.subscription.deleted. These carry the revenue and entitlement consequences.default_job or an explicit catch-all listener for unhandled types; add an alert on unprocessed-row growth.ProcessStripeWebhookJob before go-live, not after the first incident.Maintenance. Ongoing burden is low but not zero: periodic Composer updates, watching for laravel-webhook-client majors, and tracking Stripe API version upgrades. The package is small enough that a fork-and-own contingency is credible if upstream stalls — budget roughly a day of engineering to internalize it if that becomes necessary.
Support. Community open-source under MIT, maintained by Spatie but with no contractual SLA and no commercial support tier tied to this package. Escalation is GitHub issues. For a revenue-critical webhook path, that means the team carries the operational risk, not the vendor. Document this explicitly in the risk register.
Scaling. The architecture scales with queue workers, not with webhook volume — the controller's work is signature verification plus one insert, which is fast. Watch two things: the webhook_calls table grows unbounded (one row per delivery, including duplicates and replays), and querying into the JSON payload column for dedup does not index well at volume. Plan table partitioning or a scheduled prune from day one, and confirm the dedup query has an appropriate index or an extracted stripe_event_id column.
Failure modes.
exception column. Good for durability, but repeated failures will retry for days before giving up — ensure handlers are idempotent.WebhookFailed; the request is not logged. Usually a secret rotation or a multi-endpoint/Connect misconfiguration, not an attack.default_job: silent persistence with no processing. Highest-risk failure mode because nothing errors.Ramp-up. A Laravel developer is productive with the package in half a day; the integration is well-documented and conventional. The estimate that matters is domain ramp-up: engineers need to understand Stripe's event model, retry semantics, and the org's idempotency contract before writing handlers. Budget 1–2 days for package adoption and 3–5 days per tier-1 event handler including tests and replay validation.
How can I help you explore Laravel packages today?