How to robustly expose a webhook in Laravel

Let's create a Laravel client app that can expose a webhook and take related action


5 min read

How to robustly expose a webhook in Laravel

In this tutorial, we will see how to make our Laravel application capable of receiving notifications from a server app, using a simple webhook.

The app that exposes the webhook is a client app. The client part is generally much simpler than the server part. In fact, the client basically has to worry about exposing an endpoint (i.e. the webhook) and managing incoming calls.

In reality, in a robust and efficient system, the client should respond immediately and without delay. For this purpose, it is necessary to queue the received notifications and subsequently manage them with a Job. By doing so, it is also possible to perform complex and/or time-consuming tasks, without blocking the caller unnecessarily.

Thankfully, to manage all of this we don't have to rewrite everything from scratch. We will use the package: πŸ‘‰ Laravel Webhook Client

The package is developed and maintained by Spatie and we all know how much this is a guarantee!

Note: Here we won't worry about what the server app has to do, but if you are interested you can refer to the post How to manage subscribed webhooks in Laravel.

Can I trust the caller?

Can I trust the caller?

One of the main issues to be addressed on the client side concerns the reliability of the caller, namely: is the application that contacts our webhook exactly the one authorized to do so or is there some funny guy pretending to be it?

The method that we will use in this tutorial is to verify that a signature is present among the headers of the requests received and that this has been affixed using a secret key. The package will do the checking for us. It will be enough for us to configure the correct Signature Secret Key.

By default, Laravel Webhook Client package uses the class DefaultSignatureValidator to validate signatures. This is how that class will compute the signature:

hash_hmac('sha256', $request->getContent(), $signatureSecretKey);

As you can see, the validation is very easy but, at the same time, very strong.


  1. Install the Laravel Webhook Client package

  2. Open the route and handle the job

  3. Make some tests from server app

  4. Prune WebhookCall models

1. Install the Laravel Webhook Client package

Install the package:

composer require spatie/laravel-webhook-client

Publish the config file config/webhook-client.php:

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

Publish the migration:

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


php artisan migrate

2. Open the route and handle the job

Add a route in routes/web.php.

// routes/web.php

// this will open a POST route `/webhook`

IMPORTANT: Of course, you can change /webhook with others, but the correct URL must be shared with the server app.

Now, if you run php artisan route:list you can check if there is the POST entry:

Route list

Because the app that sends webhooks to you has no way of getting a csrf-token, you must add that route to the except array of the VerifyCsrfToken middleware:

// app/Http/Middleware/VerifyCsrfToken.php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
    protected $except = [
        '/webhook', // <-- add this line

Now, it's time to set in the .env file the Signature Secret Key that your server application shared with you:

# .env
# ...


If you make a test request now, you get an Exception by the package informing you that there is no valid process webhook job class:

Test request to webhook

And that's what we're going to do now!

Make the job to handle calls:

php artisan make:job ProcessWebhookJob

For the purpose of the tutorial, we simply log the received data. We can find it in $this->webhookCall:

// app/Jobs/ProcessWebhookJob.php

namespace App\Jobs;

/* use ... */
use Spatie\WebhookClient\Jobs\ProcessWebhookJob as SpatieProcessWebhookJob;

class ProcessWebhookJob extends SpatieProcessWebhookJob implements ShouldQueue
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function handle(): void
        // `$this->webhookCall` contains an instance of:
        // `\Spatie\WebhookClient\Models\WebhookCall`

        $event = \Arr::get($this->webhookCall->payload, 'event');
        $data = \Arr::get($this->webhookCall->payload, 'data', []);

        // ...
        // [perform the work here] ...

        // simply log `event` and `data`:
        logger('ProcessWebhookJob', [
            'event' => $event,
            'data'  => $data,

        // ...

Finally, we set the process_webhook_job field to indicate the Job class we just created, in the config file at config/webhook-client.php:

// config/webhook-client.php
// ...
'process_webhook_job' => \App\Jobs\ProcessWebhookJob::class,

Now, if you re-launch the test request from earlier, you successfully get an invalid signature error:

Test request to webhook

3. Make some tests from server app

πŸ‘‰ From the server app we trigger a test event:

Trigger event from server app

Note: Here we are using the server app that we created with the post How to manage subscribed webhooks in Laravel.

🎯 In the log of client app we have the correct data!

Logs in client app

πŸ‘‰ Another event triggered from server app:

Another event from server app

🎯 And in the client app log:

Logs again in client app

4. Prune WebhookCall models

With each call to our webhook, the package will add a record of the WebhookCall model. You can simply prune old records, by calling model:prune.

// app/Console/Kernel.php

namespace App\Console;

use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Spatie\WebhookClient\Models\WebhookCall;

class Kernel extends ConsoleKernel
    protected function schedule(Schedule $schedule)
        $schedule->command('model:prune', [
            '--model' => [WebhookCall::class],

        // This will not work, as models in a package are not used by default
        // $schedule->command('model:prune')->daily();

By default, records created more than 30 days ago will be removed.

You can customize this limit using the parameter delete_after_days in file config/webhook-client.php:

// config/webhook-client.php

return [
    'configs' => [
        // ...

    'delete_after_days' => 30,

✸ Enjoy your coding!

If you liked this post, don't forget to Subscribe to my newsletter!