How to robustly expose a webhook in Laravel
Tags: laravel, tutorial, webhooks, webdev
Published: 2023-09-06 15:46:59

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](https://github.com/spatie/laravel-webhook-client)
> 
> *The package is developed and maintained by* [***Spatie***](https://spatie.be/) *and we all know how much this is a guarantee!*

&nbsp;

> 💡 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](https://tonyjoe.dev/how-to-manage-subscribed-webhooks-in-laravel).

---

### Can I trust the caller?

![Can I trust the caller?](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0qimi3ne360mrhrmj6ey.jpeg)


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](https://github.com/spatie/laravel-webhook-client) uses the class [`DefaultSignatureValidator`](https://github.com/spatie/laravel-webhook-client/blob/main/src/SignatureValidator/DefaultSignatureValidator.php) to validate signatures. This is how that class will compute the signature:

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

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

---

## Steps

1. [Install the Laravel Webhook Client package](#heading-1-install-the-laravel-webhook-client-package)
    
2. [Open the route and handle the job](#heading-2-open-the-route-and-handle-the-job)
    
3. [Make some tests from server app](#heading-3-make-some-tests-from-server-app)
    
4. [Prune `WebhookCall` models](#heading-4-prune-webhookcall-models)
    

---

### 1\. Install the Laravel Webhook Client package <a name="heading-1-install-the-laravel-webhook-client-package"></a>

Install the package:

```bash
composer require spatie/laravel-webhook-client
```

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

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

Publish the migration:

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

Migrate:

```bash
php artisan migrate
```

---

### 2\. Open the route and handle the job <a name="heading-2-open-the-route-and-handle-the-job"></a>

Add a route in `routes/web.php`.

```php
// routes/web.php

Route::webhooks('/webhook');
// 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/so6090g5edtqksalurjw.png)


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:

```php
// 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:

```bash
# .env
# ...

WEBHOOK_CLIENT_SECRET="paste-here-the-signature-secret-key-shared-with-server-app"
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/50ovyiiapez8kfhxr0jq.png)



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

Make the job to handle calls:

```bash
php artisan make:job ProcessWebhookJob
```

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

```php
// 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`:

```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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/568wfio7y5f2vwg45om7.png)


---

### 3\. Make some tests from server app <a name="heading-3-make-some-tests-from-server-app"></a>

👉 From the **server app** we trigger a test event:

![Trigger event from server app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/08r890rj8c7mru3jsdo2.png)

>💡 Note: Here we are using the server app that we created with the post [How to manage subscribed webhooks in Laravel](https://tonyjoe.dev/how-to-manage-subscribed-webhooks-in-laravel).

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

![Logs in client app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8qodlsxq99b5c8i8btay.png)



---

👉 Another event triggered from **server app**:

![Another event from server app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v2c743crysvy851dln6y.png)



🎯 And in the **client app** log:

![Logs again in client app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5l14mgns62zpqcxscgdy.png)



---

### 4\. Prune `WebhookCall` models <a name="heading-4-prune-webhookcall-models"></a>

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`.

```php
// 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],
        ])->daily();
    
        // 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`:

```php
// config/webhook-client.php

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

    'delete_after_days' => 30,
];
```

---

✸ Enjoy your coding!

&nbsp;

###### If you liked this post, don't forget to add your Follow to my profile!

{% user tonyjoe %}