A local debugging and diagnostic dashboard for Laravel Cashier. Captures Stripe webhook processing, detects inconsistent billing states, and explains what went wrong.
felicianopj/laravel-cashier-inspector is a Laravel package for a local debugging and diagnostic dashboard for laravel cashier. captures stripe webhook processing, detects inconsistent billing states, and explains what went wrong..
It currently has 0 GitHub stars and 8 downloads on Packagist (latest version v0.1.4).
Install it with composer require felicianopj/laravel-cashier-inspector.
Discover more Laravel packages by felicianopj
or browse all Laravel packages to compare alternatives.
Last updated
The diagnostic toolkit for Laravel Cashier.
Laravel Cashier Inspector is a local debugging and diagnostic dashboard for Laravel Cashier. It captures Stripe webhook processing, detects inconsistent billing states, and explains what went wrong.

Cashier answers Stripe with HTTP 200 even when no billable model matches the event's customer. Stripe records a successful delivery, the application does nothing, and nothing anywhere raises an error. That is the class of failure this package exists to make visible:

Early release. Feature complete for what it sets out to do, but not yet
used widely against real Stripe traffic, so the API may still change in a
minor release while the version stays below 1.0.0.
^8.2^11.0 | ^12.0 | ^13.0^15.0 | ^16.0Cashier is a hard requirement rather than something you are expected to have already, so Composer will refuse to install this package alongside a Cashier version it does not support instead of failing later at runtime.
Every supported Laravel version is covered by the test matrix, against both lowest and stable dependency resolutions: Laravel 11 and 12 on PHP 8.2, 8.3, 8.4 and 8.5, and Laravel 13 on PHP 8.3, 8.4 and 8.5. Laravel 13 is not tested on PHP 8.2 because it requires a Pest Laravel plugin major that itself requires PHP 8.3; the package still supports PHP 8.2 on Laravel 11 and 12. Both Cashier majors are covered by the same runs: the lowest resolutions land on Cashier 15 and the stable ones on Cashier 16.
On PHP 8.5, the oldest resolution reports a deprecation for every test. It
comes from PDO::MYSQL_ATTR_SSL_CA in Laravel 11's schema state and in
Testbench's own skeleton config, not from this package, and the suite passes.
The tests run on SQLite and never touch that constant.
composer require felicianopj/laravel-cashier-inspector:^0.1 --dev
php artisan cashier-inspector:install
php artisan migrate
The install command publishes the package config and migrations, checks
that Laravel Cashier Stripe and its own database schema are present, warns
if STRIPE_WEBHOOK_SECRET isn't set, and prints the dashboard URL.
No published views or frontend assets are required for normal installation. Laravel's package discovery registers the service provider automatically.
To publish config or migrations individually:
php artisan vendor:publish --tag=cashier-inspector-config
php artisan vendor:publish --tag=cashier-inspector-migrations
The dashboard lives at /cashier-inspector by default, configurable via
CASHIER_INSPECTOR_PATH. It's enabled by default only in local environments
(CASHIER_INSPECTOR_ENABLED).
By default it shows problems only - errors, warnings, unmatched events, and processing that's taking too long - with filters and search to see everything else. New deliveries are announced as they arrive:

Search matches Stripe event, customer, subscription, invoice, and checkout
session ids, the local billable model id, and the local billable email. Email
searching works even though emails are redacted out of stored payloads: the
term is matched against your own customer table, so no copy of the address has
to be kept here. Only the model configured through Cashier
(Cashier::useCustomerModel()) is searched, on its email column.
Every event page carries a timeline of the phases of the request that delivered it, with how long each one took:

These five are the phases that can honestly be observed from outside Cashier: everything its handler does is a single window, and any listeners the application registers fall inside that window too. When Cashier has no handler for the event type, the handler phase is recorded as skipped; when it throws, as failed, carrying the exception.
Turn it off with CASHIER_INSPECTOR_RECORD_STEPS=false on an installation
with heavy webhook traffic that never reads a timeline. Deliveries recorded
while it is off, or before this feature existed, still show their received and
resolved times.
Every event page also has a "Copy diagnostic report" button that produces a sanitized, plain-text summary suitable for pasting into a GitHub issue, Discord, support, or an LLM.
Failures are captured without any of this: an exception reporting hook records the class, message and trace, and a terminating middleware records that the request ended abnormally even when the hook never runs.
That pair has one gap. An exception the application chooses not to report -
anything in dontReport, or a handler that reports selectively - leaves only
the fallback, so the event page says the request ended with a 500 and cannot
say what threw. Turn on:
CASHIER_INSPECTOR_ROUTE_MIDDLEWARE=true
and this package attaches middleware to Cashier's own webhook route. Every
Throwable it lets escape is recorded with its class, message and trace, then
rethrown unchanged, so Cashier and the application's error handling behave
exactly as before. The route is found by the controller it resolves to, so
moving Cashier with CASHIER_PATH needs no configuration here.
It does not make signature verification separately measurable - Cashier applies that from its controller's constructor, so it runs inside the route dispatch - and it does not report which local subscription Cashier resolved, which the handlers do inline with no seam to observe.
When Laravel Telescope is installed, every entry it records while a Stripe webhook is being processed - the request, the queries, the events, the exceptions - is tagged with that event's id, and each event page carries a "View in Telescope" link filtered to those entries. So a diagnosis here leads straight to the queries that ran while it happened.
Telescope is not a dependency and nothing here runs without it. The link is
hidden when Telescope is absent or disabled, and
CASHIER_INSPECTOR_TELESCOPE_LINKS=false suppresses it while leaving
Telescope alone.
Nightwatch is not integrated: it exposes no supported way to attach an identifier to the current request, and no per-request URL to link to.
The dashboard is never public by default. In production it stays disabled until explicitly enabled, and an authorization callback is required:
use FelicianoPJ\CashierInspector\CashierInspector;
CashierInspector::auth(function (Illuminate\Http\Request $request): bool {
return $request->user()?->can('viewCashierInspector') ?? false;
});
Without a callback, access is restricted to the local environment.
Webhook payloads can contain personally identifiable and commercially sensitive data. By default, these paths are redacted before a payload is ever stored:
data.object.customer_emaildata.object.customer_detailsdata.object.metadatadata.object.emaildata.object.namedata.object.phonedata.object.addressdata.object.shippingdata.object.receipt_emaildata.object.billing_detailsemail, name, phone, address, shipping and metadata are masked
under data.previous_attributes too, which carries the old values of whatever
changed. The defaults err towards masking: name is a product name on
product.* events, and is redacted there as well.
Redaction paths are configurable, support dot-notation with wildcards, and can
be disabled entirely with CASHIER_INSPECTOR_REDACTION_ENABLED, which isn't
recommended outside local development.
Raw payload storage is environment-conditional
(CASHIER_INSPECTOR_STORE_PAYLOADS): on by default in local environments, off
by default everywhere else. Exception stack traces are never stored by default
(CASHIER_INSPECTOR_STORE_EXCEPTION_TRACES).
Cashier Inspector never modifies Laravel Cashier's own tables - it only reads
from them for diagnostics, and writes to its own cashier_inspector_* tables.
Every captured event is run through a set of rules. A rule that triggers
records a finding against the event, which then shows on the event page, in
the copied diagnostic report, and in cashier-inspector:check. Warning and
error findings also make the event count as a problem, so it stays in the
dashboard's default view.
These rules ship with the package:
| Rule | Code | Severity | Triggers when |
| --- | --- | --- | --- |
| ProcessingExceptionRule | processing_exception | error | Cashier threw while handling the webhook |
| IncompatibleCashierSchemaRule | cashier_schema_incompatible | error | Cashier's own tables or columns are missing |
| MissingWebhookSecretRule | webhook_secret_missing | warning | STRIPE_WEBHOOK_SECRET is not configured |
| DuplicateDeliveryRule | duplicate_delivery | warning | The same Stripe event was delivered more than once |
| TestLiveModeMismatchRule | mode_mismatch | warning | The event's mode does not match the configured Stripe key |
| MissingLocalSubscriptionRule | missing_local_subscription | warning | The event names a subscription Cashier has no row for |
| MissingBillableModelRule | missing_billable_model | warning | The event's customer resolves to no local billable model |
| DuplicateSubscriptionTypeRule | duplicate_subscription_type | warning | One billable model has two valid subscriptions of the same type |
| SlowProcessingRule | slow_processing | warning | Processing took longer than the configured threshold |
| SubscriptionStatusMismatchRule | subscription_status_mismatch | warning | The local subscription status differs from Stripe's |
| SubscriptionPriceMismatchRule | subscription_price_mismatch | warning | The local subscription prices differ from Stripe's |
| UnhandledWebhookRule | webhook_unmatched | info | Cashier has no handler for this event type |
The two mismatch rules compare local Cashier state against a live fetch from
Stripe. They are opt-in and off by default
(CASHIER_INSPECTOR_STRIPE_API_CHECKS), since enabling them makes a live
Stripe API call synchronously, during webhook processing.
Rules are a list in the published config: remove one and it stops running, add your own and it runs alongside the rest. See EXTENDING.md for the contract and a worked example.
php artisan cashier-inspector:check
Reports whether the pieces this package depends on are in place: Cashier and its schema, Cashier Inspector's own tables, the Stripe secret and webhook secret, Cashier's billable customer model, whether any webhook events have arrived recently, and what the diagnostic rules have already found on the events still stored.
INFO Cashier Inspector's own tables exist.
INFO STRIPE_SECRET is configured.
INFO STRIPE_WEBHOOK_SECRET is configured.
INFO Billable model was detected [App\Models\User].
INFO 4 webhook events were received in the last 24 hours.
ERROR Diagnosed on the events that are still stored: 1 processing_exception.
It exits non-zero only when something is genuinely broken, so it can gate a deploy. A missing webhook secret is reported as a warning and still exits zero
The recent-events window defaults to 24 hours
(CASHIER_INSPECTOR_RECENT_EVENTS_WINDOW_HOURS).
php artisan cashier-inspector:event evt_123
Prints what was captured and diagnosed for a single Stripe event, followed by every delivery attempt with its status, severity, and duration. The summary is the same text the dashboard's "Copy diagnostic report" button produces, and neither includes the raw payload.
Useful where the dashboard is not reachable - an SSH session on a production box, or a CI job. Exits non-zero when no event was captured for that id.
Searching by customer id finds every event recorded against that customer, so
an event stored without one is invisible to that search. Some Stripe objects
reference no customer at all - an invoice_payment names only its invoice -
and an event captured before a billable model had its stripe_id set matched
nothing at the time.
php artisan cashier-inspector:backfill-customers
This fills both, and only ever fills blanks. Run it after setting a
stripe_id on an existing customer, or after a burst of webhooks arrived out
of order. It also re-reads stored payloads to recover correlation ids from
events captured by a version of this package that was not yet reading them,
which is what an existing installation needs after upgrading.
An event whose payload names no customer, invoice or subscription - a charge created outside any customer - has nothing to correlate through, and its customer column stays empty.
Old events, deliveries, and diagnostics can be pruned:
php artisan cashier-inspector:prune
This isn't scheduled automatically. Add it to the application's own scheduler to run it regularly:
$schedule->command('cashier-inspector:prune')->daily();
The retention period defaults to 7 days (CASHIER_INSPECTOR_RETENTION_DAYS)
and can be overridden per run with --days.
TROUBLESHOOTING.md covers the recurring Cashier and Stripe webhook failures: what each looks like in the dashboard, what causes it, and how to fix it. Each section names the finding this package records, so a diagnosis on an event page leads straight to the explanation.
See CHANGELOG.md for what shipped in each release.
Laravel Cashier Inspector is an independent open-source project and is not affiliated with or endorsed by Laravel LLC or Stripe, Inc.
MIT