Open-source project
spatie/laravel-webhook-client avatar
spatie/laravel-webhook-client

laravel-webhook-client: receiving webhooks in Laravel with signature checks and a queue

Receive webhooks in Laravel apps

1,204 stars160 forksPHPMIT

At a glance

What is it?
This Spatie package gives a Laravel app an endpoint for receiving webhooks, verifying the signature against a shared secret, storing the call, and processing the payload in a queued job. Multiple receiving endpoints are configured as a list, and each one names its own secret, validator, profile, response, and job.
Who is it for?
laravel-webhook-client fits a Laravel team that receives webhooks from several providers, each with its own signing scheme, and that wants signature verification and payload storage to be handled rather than hand-rolled in a controller. It is the wrong tool if you only have one provider with one simple contract, since the configuration is heavier than a plain route.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Receiving is the half of webhooks most packages skip

A webhook is a way for an app to provide information to another app about a specific event, and the two apps communicate with a simple HTTP request. The package description for this one is receiving webhooks in Laravel apps.

That direction is worth pausing on, because most webhook packages are about sending. The project summary makes the split explicit: if you need to send webhooks, take a look at the laravel-webhook-server package. This one is the other half.

The features are the three you would have to write yourself otherwise. Support for verifying signed calls, storing payloads and processing the payloads in a queued job, and a signature check that rejects a tampered payload.

The signature verification is the part that matters most for security. The configuration expects that every webhook call will be signed using a secret, and that secret is used to verify that the payload has not been tampered with. A webhook endpoint that accepts unsigned requests is an open endpoint that anyone can POST to, and a package that makes the signed path the default rather than an option is doing the right thing.

The queued processing is the part that matters for reliability. The package will try to store and respond to the webhook as fast as possible, and the processing of the payload of the request is done via a queued job. It is recommended to not use the `sync` driver but a real queue driver.

That recommendation is a small design decision with a large consequence. A synchronous implementation would hold the HTTP connection open while the payload is processed, which means the sender waits, which means senders time out and retry, which means duplicate deliveries. Queueing lets the endpoint acknowledge immediately and process afterwards, at the cost of the application having to be idempotent about processing the same call twice.

Configuration is a list of endpoints, each fully specified

The config file is published to `config/webhook-client.php` and its structure is the thing worth understanding, because it decides how the package scales to more than one provider.

The top level is a `configs` array, and each entry is a named receiving endpoint. The comment on the first one explains why: this package supports multiple webhook receiving endpoints, and if you only have one endpoint receiving webhooks, you can use `default`.

Each endpoint config names six things. The `name` identifies the endpoint. The `signing_secret` is the shared secret, taken from an environment variable, `env('WEBHOOK_CLIENT_SECRET')`, and the comment says this value should be provided by the app that will send you webhooks. The `signature_header_name` is the name of the header containing the signature, `Signature` by default. The `signature_validator` is the class that verifies the content of the signature header, and it should implement `\Spatie\WebhookClient\SignatureValidator\SignatureValidator`.

The `webhook_profile` determines if the webhook call should be stored and processed, with `ProcessEverythingWebhookProfile` as the default. The `webhook_response` determines the response on a valid webhook call, `DefaultRespondsTo` by default. The `webhook_model` is the classname of the model used to store webhook calls, which should be equal to or extend `Spatie\WebhookClient\Models\WebhookCall`.

Then there is `store_headers`, an array of the headers that should be stored on the webhook call model when a webhook comes in, and to store all headers you set that value to `*`. And `process_webhook_job` is the class name of the job that will process the webhook request, which should be set to a class extending `Spatie\WebhookClient\Jobs\ProcessWebhookJob` that has a `handle` method.

So the design is: one array entry per provider, and each entry carries the whole contract for that provider, including which validator, which response, and which job. Adding a second provider with a different signing scheme is a matter of adding an entry with a different validator, not of branching in your controller.

Above the array there is one more setting. `delete_after_days` is the number of days after which models should be deleted, set to null if no models should be deleted, and it defaults to 30.

Installation, migration, and the artisan commands

The setup is three steps, and they are the standard Spatie shape of a package that owns a model and a route.

Install it with composer:

bash
composer require spatie/laravel-webhook-client

Publish the config file:

bash
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-config"

Then the database. By default, all webhook calls will get saved in the database, and the migration is published with:

bash
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations"

After the migration has been published, you can create the `webhook_calls` table by running the migrations:

bash
php artisan migrate

The ordering matters, and it is the ordering any Spatie package with a model follows. The config has to be published before you can set your secret, and the migration has to be published before you can migrate, and neither works if you have not installed the package.

The `delete_after_days` setting ties into this. Since the calls are stored, the table grows, and the retention setting deletes old rows on a schedule. Setting it to null means nothing is ever deleted, which is the setting to pick if you are treating the webhook log as an audit record rather than as a processing buffer.

The `store_headers` setting is the one to think about before you enable it broadly. Storing all headers with `*` means the stored record contains whatever the sender sent, which for a webhook that carries an authorization token means the token is in your database. That is a reasonable thing to want for debugging, and a reasonable thing to regret later.

Routing, and the CSRF exclusion you have to remember

The routing is one line, and it comes with a consequence that every user has to deal with.

In the routes file of your app, you must pass the route to `Route::webhooks`:

php
Route::webhooks('webhook-receiving-url');

Behind the scenes, by default this will register a `POST` route to a controller provided by this package.

And then the important sentence: because the app that sends webhooks to you has no way of getting a csrf-token, you must exclude the route from csrf token validation.

That is a genuine security trade-off, and the reason it exists is structural rather than lazy. CSRF protection works by requiring the request to carry a token that only a page rendered by your application could have given the browser. A webhook sender is not a browser and has no such token, so the token check cannot apply to it. The protection that replaces it is the signature check, which proves the request came from someone who knows the shared secret.

The project shows how to do the exclusion in recent versions of Laravel, using the middleware configuration in `bootstrap/app.php` with the `Application::configure` style, importing `Illuminate\Foundation\Application` and `Illuminate\Foundation\Configuration\Middleware` and then excluding the route from the `validateCsrfTokens` middleware.

The exemption is safe only because the signature check is in place, which is why the config makes the secret mandatory in practice even though the file itself will publish with the environment variable unset. If you set the secret to null, you have turned a signed endpoint into an unsigned one, and the CSRF exemption is then the only thing standing between you and arbitrary POSTs.

The route naming also explains the multi-endpoint design. `Route::webhooks` takes the URL, and the config list holds the endpoints, so a provider that needs a different secret, a different validator, or a different response is registered as its own entry with its own route.

What the project does not do, and the other half of the pair

The boundaries are worth stating before you install, because the name of the package is narrower than you might expect.

It does not send webhooks. The project summary points you to the laravel-webhook-server package for that, and the split is clean: one package for the outbound side, one for the inbound side. That is worth knowing because a team that assumed this one did both would find themselves writing the sending half by hand.

It does not abstract away the provider. There is no concept of a Stripe webhook or a GitHub webhook in the configuration. What you get is a generic signed-call receiver with a place to put a per-provider validator, a per-provider response, and a per-provider job. The provider-specific knowledge lives in your own classes.

That is the right level of abstraction for a package like this, and it is also the limit of it. A team with ten providers will write ten validator classes, or one validator that switches on the endpoint name, and either way the provider knowledge is theirs.

The queued job is the extension point that matters most, because it is where your business logic goes. A valid job is any class extending `Spatie\WebhookClient\Jobs\ProcessWebhookJob` that has a `handle` method. The job receives the stored webhook call, and it runs outside the request, so it can be slow, it can retry, and it can fail without the sender ever knowing.

The webhook profile is the other extension point and it is subtler. It determines if the webhook call should be stored and processed, and the default processes everything. A profile lets you filter before the payload is persisted, which is the right place to drop traffic you do not want, since a webhook endpoint that accepts and stores everything is a storage problem as well as a security one.

The webhook response class is the third, and it decides what you send back to the provider on a valid call. A provider that expects a 200 and gets a 202 will retry, and a provider that expects a body will fail to parse one it does not get, so this is the piece where you match the other side's expectations.

The repository layout is what you would expect from a Spatie package. There is `src/`, `config/`, `database/` for the migration, `tests/`, a `composer.json`, a `phpunit.xml.dist`, a `phpstan.neon.dist` with a `phpstan-baseline.neon`, a `.php-cs-fixer.dist.php` for code style, an `UPGRADING.md`, a `CHANGELOG.md`, and a `LICENSE.md`.

The licence is MIT, the primary language is PHP, and the release history shows steady maintenance: 3.6.2 in April 2026, 3.6.3 in June 2026, and 3.7.0 in July 2026, with the last push on the same day as the 3.7.0 tag. There is a blog post linked as the homepage, on sending and receiving webhooks in Laravel apps, which is the long-form version of the same material.

Editorial conclusion

laravel-webhook-client fits a Laravel team that receives webhooks from several providers, each with its own signing scheme, and that wants signature verification and payload storage to be handled rather than hand-rolled in a controller. It is the wrong tool if you only have one provider with one simple contract, since the configuration is heavier than a plain route. Check three things before you install it: that your provider's signing scheme matches what the configured signature validator expects, since the default one assumes a particular header and format, that you set a real queue driver rather than `sync`, because the package recommends against it, and that the `delete_after_days` value suits your retention policy, since the stored calls are deleted on that schedule by default.

Frequently asked questions

How do I install and configure spatie/laravel-webhook-client?

Run `composer require spatie/laravel-webhook-client`, publish the config with `php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-config"`, publish the migration with the same command and the `webhook-client-migrations` tag, and run `php artisan migrate` to create the `webhook_calls` table. Then set your `signing_secret` in the published config.

How do I register the webhook route in Laravel?

Pass the URL to `Route::webhooks` in your routes file, for example `Route::webhooks('webhook-receiving-url')`, which registers a POST route to a controller provided by the package. Because the sending app has no way of getting a csrf-token, you must exclude that route from csrf token validation.

What do the signature validator, webhook profile, and webhook response config keys do?

The `signature_validator` is the class that verifies the content of the signature header and should implement the package's SignatureValidator interface. The `webhook_profile` determines if the webhook call should be stored and processed, with ProcessEverythingWebhookProfile as the default. The `webhook_response` determines the response on a valid webhook call, with DefaultRespondsTo as the default.

Does spatie/laravel-webhook-client send webhooks as well as receive them?

No, it only receives them. The project summary points to the separate laravel-webhook-server package for sending webhooks. The client package handles signature verification, storing the call, and processing the payload in a queued job, and the package recommends a real queue driver rather than the `sync` driver.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. spatie/laravel-webhook-client on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/spatie-laravel-webhook-client.svg)](https://hysenlabs.com/projects/spatie-laravel-webhook-client)

Was this useful?