# 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](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!*

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>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 </em><a target="_blank" rel="noopener noreferrer nofollow" href="https://tonyjoe.dev/how-to-manage-subscribed-webhooks-in-laravel" style="pointer-events: none"><em>How to manage subscribed webhooks in Laravel</em></a><em>.</em></div>
</div>

---

### Can I trust the caller?

![Can I trust the caller?](https://cdn.hashnode.com/res/hashnode/image/upload/v1693950272339/795e0355-1b07-4b4e-807c-7ad75868f61a.jpeg align="center")

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

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

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://cdn.hashnode.com/res/hashnode/image/upload/v1693948397757/f2277519-53f7-4421-91f4-64faca4f6b07.png align="center")

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://cdn.hashnode.com/res/hashnode/image/upload/v1693948787831/dd77eda3-5903-478a-af88-ac9d6d8e8e7e.png align="center")

*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://cdn.hashnode.com/res/hashnode/image/upload/v1693949314345/0427094c-d5f8-4d04-a729-cd4d40c27807.png align="center")

---

### 3\. Make some tests from server app

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

![Trigger event from server app](https://cdn.hashnode.com/res/hashnode/image/upload/v1693952596013/2c3e85f3-b749-4eb5-a479-6636228c18a1.png align="center")

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>Note: Here we are using the server app that we created with the post </em><a target="_blank" rel="noopener noreferrer nofollow" href="https://tonyjoe.dev/how-to-manage-subscribed-webhooks-in-laravel" style="pointer-events: none"><em>How to manage subscribed webhooks in Laravel</em></a><em>.</em></div>
</div>

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

![Logs in client app](https://cdn.hashnode.com/res/hashnode/image/upload/v1693952732245/aa8920a9-fcb9-48fd-b2e7-d02ff1a3e40e.png align="center")

---

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

![Another event from server app](https://cdn.hashnode.com/res/hashnode/image/upload/v1693953292771/d2794e10-205c-4383-8161-8b13f91b7663.png align="center")

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

![Logs again in client app](https://cdn.hashnode.com/res/hashnode/image/upload/v1693953465928/8009e99c-2335-4efb-8ea0-dc23f6e2d5bb.png align="center")

---

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

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

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