amjadiqbal / laravel-webhook-console
Dashboard and replay tool for spatie/laravel-webhook-client: browse every received webhook by config, status and date, inspect payloads and headers with secrets masked, see failures (stored exceptions and failed queue jobs), replay one or bulk-retry failed webhooks, prune old calls, plus webhooks:fa
Package info
github.com/amjadiqbal/laravel-webhook-console
pkg:composer/amjadiqbal/laravel-webhook-console
Requires
- php: ^8.3
- illuminate/bus: ^12.0|^13.0
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/routing: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- illuminate/view: ^12.0|^13.0
- spatie/laravel-webhook-client: ^3.4
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Laravel Webhook Console: a dashboard and replay tool for spatie/laravel-webhook-client
spatie/laravel-webhook-client stores every webhook your
Laravel app accepts in the webhook_calls table, but it has no screen to look at them. This package adds one:
a gated dashboard that lists your Stripe, GitHub, Shopify or any other webhooks by config, status and date,
shows each payload and its headers with secrets masked, shows why a webhook failed, and lets you
replay one call or bulk-retry every failure with a click or from artisan.
It is read-only by default. Replay and prune each sit behind their own gate.
Docs and demo: https://amjad.com.pk/laravel/packages/laravel-webhook-console
composer require amjadiqbal/laravel-webhook-console
Then open /webhook-console in your local environment.
Why failures are hard to see without it
webhook-client writes an exception on a call only when dispatching its process job fails, or when the
job throws on the sync queue. When your job throws on a real queue worker, which is the usual case in
production, the call keeps exception = NULL and the failure only exists in failed_jobs, with nothing that
points back to the webhook.
The console reads both. It links each failed_jobs row to its webhook call (from the serialized job's model
id, read with a regex and never unserialized), so every call has one of three statuses:
| Status | Meaning |
|---|---|
| Failed | webhook-client stored an exception on the call |
| Failed in queue | the process job failed on a queue worker and is in failed_jobs |
| No error | nothing failed that the console can see |
Requirements
- PHP 8.3+, Laravel 12 or 13
spatie/laravel-webhook-client3.4+ (installed for you), already receiving webhooks- Queue failures are read from the
databaseordatabase-uuidsfailed job driver (Laravel's default). Other drivers still get everything else; the dashboard says why queue failures are missing.
Access: three gates
| Gate | Allows | Default |
|---|---|---|
viewWebhookConsole |
the dashboard, payloads, headers, failures | local environment only |
replayWebhookCalls |
Replay, Replay selected, Retry failed | local environment only |
pruneWebhookCalls |
deleting old calls from the dashboard | local environment only |
Outside local, nobody gets in until you define the gates, for example in AppServiceProvider::boot():
use App\Models\User; use Illuminate\Support\Facades\Gate; Gate::define('viewWebhookConsole', fn (User $user) => $user->is_admin); Gate::define('replayWebhookCalls', fn (User $user) => $user->is_admin); Gate::define('pruneWebhookCalls', fn (User $user) => false); // view and replay only
A guest never passes a gate that type-hints User. Someone who can view but not replay sees the dashboard
in read-only mode, without the replay buttons; the replay and prune routes return 403 for them.
What you can do
List and filter. Every stored call with its config name, event (Stripe's type, GitHub's action,
Shopify's topic...), status, error and age. Filter by config, status, date range, or search inside the
payload and URL (an event id, a customer email, an order number). Works on MySQL, MariaDB, PostgreSQL and
SQLite.
Inspect one call. The payload as pretty JSON with a copy button, the stored headers, uploaded file names
and sizes (never their content), the stored exception with its stack trace, and the failed_jobs exception
for queue failures.
Masking. Payload keys, header names and URL query parameters whose name contains password, secret,
token, authorization, signature, api_key, cookie, card_number, cvc, iban and similar are
shown as ••••••, at any depth. The signature header of every webhook-client config is always masked. The
list is in the config file. Masked values stay masked in the copy button too.
Replay. Dispatches the call's configured process_webhook_job again, exactly as webhook-client does for a
new webhook: the job from the call's config, clearException(), dispatch(), and saveException() if
dispatching throws. Nothing is sent over the network and the signature is not checked again; the stored
call is simply processed again, so your job should be safe to run twice for the same event (most webhook
handlers already are, because providers retry too).
Bulk retry. Tick calls and click Replay selected, or click Retry failed to replay every failure that
matches the current filters (up to bulk_limit, 100 by default). Calls are replayed oldest first.
Prune. Delete calls older than N days, for all configs or one. webhook-client can also do this on a
schedule with php artisan model:prune --model="Spatie\WebhookClient\Models\WebhookCall", using its
delete_after_days setting.
Artisan
php artisan webhooks:failed # failed calls (stored exception or failed queue job) php artisan webhooks:failed --since=24h --strict # exit code 1 when anything failed: CI, cron, monitoring php artisan webhooks:failed --name=stripe --json php artisan webhooks:replay 41 42 # replay these calls php artisan webhooks:replay --failed --dry-run # what would be replayed php artisan webhooks:replay --failed --since=2h --force
webhooks:replay asks before replaying; non-interactive runs need --force. It exits 1 when any replay
could not be dispatched, so it is safe to use in scripts. To be told about failures, schedule the check:
// routes/console.php Schedule::command('webhooks:failed --since=1h --strict')->hourly()->emailOutputOnFailure('ops@example.com');
Custom webhook models
Each webhook-client config can set its own webhook_model. The console reads every config:
- A call is replayed through its own config's model, so a job that type-hints your model gets that model.
- If configs use models with different tables, the dashboard shows a table switcher and
webhooks:replay --source=<table>picks one;webhooks:failedcovers all of them. - Calls whose config name no longer exists are still listed, but can't be replayed (the dashboard says why).
The same goes for the published default config, whose
process_webhook_jobis empty.
Configuration
php artisan vendor:publish --tag=webhook-console-config
| Key | Default | |
|---|---|---|
dashboard.enabled |
true (WEBHOOK_CONSOLE_DASHBOARD) |
false removes the routes |
dashboard.path |
webhook-console (WEBHOOK_CONSOLE_PATH) |
|
dashboard.domain |
null (WEBHOOK_CONSOLE_DOMAIN) |
|
dashboard.middleware |
['web'] |
the view gate is always added after these |
dashboard.per_page |
50 |
|
mask |
password, secret, token, ... | words matched inside key and header names |
failed_jobs.enabled |
true |
read failed_jobs for queue failures |
failed_jobs.scan_limit |
1000 |
newest failed_jobs rows read per request |
bulk_limit |
100 |
the most calls one bulk retry replays |
The views can be published with --tag=webhook-console-views.
How a replayed queue failure stops counting
Replaying touches the call (updated_at). A failed_jobs row older than the call's updated_at belongs to
an earlier attempt, so the call shows as No error again, with the old failure still visible on its page as
Earlier queue failure (replayed since). If the replayed job fails again, a newer row appears and the call is
Failed in queue again. The failed_jobs rows themselves are never changed or deleted.
Testing
composer test # Pest, on SQLite scripts/integration.sh # the same suite on real MySQL 8.4 and PostgreSQL 17 (Docker) scripts/ci-local.sh # PHP 8.3/8.4/8.5 x Laravel 12/13, Pint, PHPStan, then the integration run
Need this built into your app, or something custom?
I build and maintain Laravel apps that live on webhooks: payment providers, GitHub and Shopify apps, and the queue workers behind them. If you would rather have someone set up reliable webhook handling with you, or you need something this package does not do, get in touch.
Support
- 🐛 Bug or feature request: open an issue
- 🔒 Security issue: please report privately via GitHub's security tab
Need this customised, or something built?
I'm available for custom development, package integration, and technical consulting.
More packages
Part of a family of open-source packages. See all of them.
| laravel-mysql-advisor | MySQL and MariaDB health checks with the SQL to fix each problem |
| kiln | Deploy-time OPcache control for Laravel |
| laravel-logpulse | Log health monitoring and alerting |
| laravel-driver-js | Product tours and onboarding for Laravel |
Changelog
See CHANGELOG.md.
License
MIT. See LICENSE.md. This package is independent and is not made or endorsed by Spatie;
spatie/laravel-webhook-client is their MIT-licensed package.
