Handling a webhook request is a tricky task. In this article, I will you take through how the minority of developers handle webhook calls and a more preferred way of handling a webhook call - MY PREFERRED WAY THOU.
Before going too far, what the heck is Webhook?
Simply put, a webhook is way of notifying an external system that an event has happened in your application.
Technically, it’s a process whereby an application sends an HTTP request to another application whenever an event happens in the application. Some call it reveresed API
In this case, your application is the receiver while the other application is the sender (where the event was initialized).
Since webhook is all about sending and receiving HTTP requests, as a receiver, you can decide to set up a route that will receive the request, perform logic with the request, return a status of code of 2XX, and listen for another request.
Yes, you are right, it will work but what if the logic you will be performing each time a request is being sent requires a lot of CPU processes - for example, sending an email or looping through thousands of records. This might not be a big deal if the webhook call has a long interval in between say an hour.
But what if the interval between the calls is short say a minute interval, then handling a heavy operation becomes a headache for your application.
So, if that is your concern, then I have a gift for you - just keep reading.
As said earlier, we discuss two methods of handling a webhook call.
How to Handle Webhook call in Laravel
Method 1
Step 1: We will create an endpoint to receive the webhook call in the web.php in the routes folder.
In most cases, the webhook call will be sent through a POST request unless the sender explicitly uses other request types, so we will be using a post route.
You should add this to your web.php file
Route::post('webhook', [WebHookController::class, 'webhookHandler']);
Step 2: Next is to create a controller php artisan make:controller WebHookHandler and add a method webhookHandler to it
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class WebHookController extends Controller
{
/**
* @param Request $request
* @return json
*/
public function webhookHandler(Request $request){
// We have access to the request body here
// So, you can perform any logic with the data
// In my own case, I will add the delay function
sleep(50); //this will delay the script for 50 seconds
return response()->json('ok');
}
}
For the sending application to consider the webhook as a successful one you need to respond with a status of 2XX that is why I added the return response()->json(‘ok’)
🎯 We just handled a webhook call with just two steps but this method doesn’t scale well, So, I won’t recommend it
Method 2
Here comes my gift for you. In this method, I will introduce to you a laravel package that handles webhook calls efficiently - Laravel-webhook-client
This package has supports for verifying signed calls, storing payloads, and processing the payloads in a queue job immediately or later.
Below are some of the features of this package:
Verifying the signature of incoming webhooks
By default, the package assumes that the request header has a signature to verify the data being sent. So, if the signature of the request is valid - process it - if not, it will throw an exception and fire off the InvalidSignatureEvent event. You can listen to this if needed. Requests with invalid signatures will not be stored in the database by default but you can customize this to suit your need.
Determining which webhook requests should be stored and processed
Once the request is verified by the signature_validator, it passes the request to a webhook profile.
A webhook profile is a class that determines if a request should be stored and processed by your app. It allows you to filter out webhook requests that are of interest to your app.
If the webhook profile determines that the request should be stored and processed, it will be first stored, in the webhook_calls table. After that, the newly created webhook record will be passed to the queue job - this feature makes it easy to handle logic that requires a lot of processes by putting them in a queue and giving responses as fast as possible while listening to other requests from the webhook.
After the job has been dispatched to the queue, the request will be passed to a webhook response. A webhook response is a class that determines the HTTP response for the request. An ‘ok’ message response with 200 status code is returned by default.
Creating your own webhook response
This package allows you to create your own response which most time the sender doesn’t care about as far as you return with a status code in the range of 2XX
Handling incoming webhook requests for multiple apps
Your application might be receiving a webhook request from more than one application, this package, allow you to define your configuration for each sender.
Using the package with an existing controller
If you have an existing controller that you would like to handle the webhook call - you can programmatically add support for webhooks to your own controller.
Let’s get our hands dirty by creating a simple project that receives webhook calls from two different application and pass the requests into a queue and process them as jobs.
Installation and Configuration
I assumed you have set up your new laravel project if not you can get started here
So, let’s install the package by running:
composer require spatie/laravel-webhook-client
Let’s proceed further by publishing the configuration file:
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-config"
This will create a new file called “webhook-client.php” in the config folder
The “webhook-client.php” will look like this:
<?php
return [
'configs' => [
[
/*
* This package supports multiple webhook receiving endpoints. If you only have
* one endpoint receiving webhooks, you can use 'default'.
*/
'name' => 'default',
/*
* We expect that every webhook call will be signed using a secret. This secret
* is used to verify that the payload has not been tampered with.
*/
'signing_secret' => env('WEBHOOK_CLIENT_SECRET'),
/*
* The name of the header containing the signature.
*/
'signature_header_name' => 'Signature',
/*
* This class will verify that the content of the signature header is valid.
*
* It should implement \Spatie\WebhookClient\SignatureValidator\SignatureValidator
*/
'signature_validator' => \Spatie\WebhookClient\SignatureValidator\DefaultSignatureValidator::class,
/*
* This class determines if the webhook call should be stored and processed.
*/
'webhook_profile' => \Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile::class,
/*
* This class determines the response on a valid webhook call.
*/
'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultRespondsTo::class,
/*
* The classname of the model to be used to store webhook calls. The class should
* be equal or extend Spatie\WebhookClient\Models\WebhookCall.
*/
'webhook_model' => \Spatie\WebhookClient\Models\WebhookCall::class,
/*
* In this array, you can pass the headers that should be stored on
* the webhook call model when a webhook comes in.
*
* To store all headers, set this value to `*`.
*/
'store_headers' => [
],
/*
* The class name of the job that will process the webhook request.
*
* This should be set to a class that extends \Spatie\WebhookClient\Jobs\ProcessWebhookJob.
*/
'process_webhook_job' => '',
],
],
];
Preparing the database
By default, all webhook calls get saved into the database. So, we need to publish the migration that will hold the records.
So run:
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations"
This will create a new migration file in the “database/migration” folder.
Then run php artisan migrate
to run the migration.
Implementation
For this simple application, we will use the default signature_validator, webhook_profile and create a process_webhook_jobto handle the job for the webhook calls that will be sent from the first application and create our own signature_validator, webhook_profile, and also process_webhook_job to process webhook calls sent from the second application.
Let’s update our “webhook-client.php” to capture the two applications.
<?php
return [
'configs' => [
/**
* Configuration details for first application
*/
[
/*
* This package supports multiple webhook receiving endpoints. If you only have
* one endpoint receiving webhooks, you can use 'default'.
*/
'name' => 'application-one',
/*
* We expect that every webhook call will be signed using a secret. This secret
* is used to verify that the payload has not been tampered with.
*/
'signing_secret' => env('WEBHOOK_CLIENT_SECRET_FOR_APPLICATION_ONE'),
/*
* The name of the header containing the signature.
*/
'signature_header_name' => 'Signature',
/*
* This class will verify that the content of the signature header is valid.
*
* It should implement \Spatie\WebhookClient\SignatureValidator\SignatureValidator
*/
'signature_validator' => \Spatie\WebhookClient\SignatureValidator\DefaultSignatureValidator::class,
/*
* This class determines if the webhook call should be stored and processed.
*/
'webhook_profile' => \Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile::class,
/*
* This class determines the response on a valid webhook call.
*/
'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultRespondsTo::class,
/*
* The classname of the model to be used to store webhook calls. The class should
* be equal or extend Spatie\WebhookClient\Models\WebhookCall.
*/
'webhook_model' => \Spatie\WebhookClient\Models\WebhookCall::class,
/*
* In this array, you can pass the headers that should be stored on
* the webhook call model when a webhook comes in.
*
* To store all headers, set this value to `*`.
*/
'store_headers' => [
],
/*
* The class name of the job that will process the webhook request.
*
* This should be set to a class that extends \Spatie\WebhookClient\Jobs\ProcessWebhookJob.
*/
'process_webhook_job' => \App\Handler\WebhookJobHandlerForAppOne::class,
,
],
/**
* Configuration details for second application
*/
[
'name' => 'application-two',
/*
* We expect that every webhook call will be signed using a secret. This secret
* is used to verify that the payload has not been tampered with.
*/
'signing_secret' => env('WEBHOOK_CLIENT_SECRET_FOR_APPLICATION_TWO'),
/*
* The name of the header containing the signature.
*/
'signature_header_name' => 'Signature',
/*
* This class will verify that the content of the signature header is valid.
*
* It should implement \Spatie\WebhookClient\SignatureValidator\SignatureValidator
*/
'signature_validator' => \Spatie\WebhookClient\SignatureValidator\DefaultSignatureValidator::class,
/*
* This class determines if the webhook call should be stored and processed.
*/
'webhook_profile' => \Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile::class,
/*
* This class determines the response on a valid webhook call.
*/
'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultRespondsTo::class,
/*
* The classname of the model to be used to store webhook calls. The class should
* be equal or extend Spatie\WebhookClient\Models\WebhookCall.
*/
'webhook_model' => \Spatie\WebhookClient\Models\WebhookCall::class,
/*
* In this array, you can pass the headers that should be stored on
* the webhook call model when a webhook comes in.
*
* To store all headers, set this value to `*`.
*/
'store_headers' => [
],
/*
* The class name of the job that will process the webhook request.
*
* This should be set to a class that extends \Spatie\WebhookClient\Jobs\ProcessWebhookJob.
*/
'process_webhook_job' => '',
],
],
];
PS: Make sure to add WEBHOOK_CLIENT_SECRET_FOR_APPLICATION_ONE to your .env file. The sender should be able to provide the secret key for you.
Before we set up our job handler, let’s set up our queue system
Go to your “.env” file and set the QUEUE_CONNECTION = database
- you can decide to use other connections like Redis.
Let’s create our jobs table by running
php artisan queue:table
and then run the migration using
php artisan migrate
Laravel version ≥ 8.0 by default, already has a failed_jobs migration file, if in case it’s missing in your migration folder - you can run php artisan queue:failed-table
. Don’t forget to run the migration again.
You can read more about Laravel queue here
So, Let’s proceed to create a handler to process the webhook jobs for the first application.
So, create a folder and name it “Handler” in your “app” folder. Then create a file “WebhookJobHandlerForAppOne.php”. Then copy and paste the code below.
<?php
namespace App\Handler;
use Spatie\WebhookClient\Jobs\ProcessWebhookJob;
class WebhookJobHandlerForAppOne extends ProcessWebhookJob
{
/**
* The number of seconds the job can run before timing out.
*
* @var int
*/
public $timeout = 120;
public function handle()
{
//You can perform an heavy logic here
logger($this->webhookCall);
sleep(10);
logger("I am done");
}
}
This handler extends the ProcessWebhookJob class, which holds the WebhookCall variables containing each job’s detail.
PS: I am only logging the details and delaying the script for 10 seconds; in your own case, you might want to run heavy logic here.
The next step is to set up a route file to accept the webhook call.
Hence, add the below line of code to your web.php
Route::webhooks('webhook-url-1', 'application-one');
You might be asking why not post instead of webhook?. Behind the scenes, this will register a POST route to a controller provided by this package.
Since the app that sends the webhooks has no way of getting a csrf-token, so, it’s required you add that route to the except array of the VerifyCsrfToken middleware. Go to “app/Http/Middleware” folder and update the VerifyCsrfToken
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
'webhook-url-1'
];
With all these in place, your application is ready to receive the webhook call from the first application.
PS: Don’t forget to run php artisan queue:listen
to process the jobs.
So far, we have been able to receive a webhook from the first application, let’s begin the setup for our second application. We will be using a custom signature_validator, webhook_profile, and also a process_webhook_job .
Thus, in our “Handler” folder, we will create three files - one to handle to signature_validator , one to handle webhook_profile, which determines the payload that will be saved into the database, and one process_webhook_job to process the webhook jobs.
Below are the copies of each file:
<?php
namespace App\Handler;
use Exception;
use Illuminate\Http\Request;
use Spatie\WebhookClient\WebhookConfig;
use Spatie\WebhookClient\SignatureValidator\SignatureValidator;
class WebHookSignerHandlerForAppTwo implements SignatureValidator
{
/**
* Verify request signature
* @param Request $request
* @param WebhookConfig $config
* @return bool
*/
public function isValid(Request $request, WebhookConfig $config): bool
{
$signature = $request->header($config->signatureHeaderName);
/**
* If the sender don't add signature and you trust it- you can return true -- I won't recommend this
*/
if (! $signature) {
return false;
}
$signingSecret = $config->signingSecret;
if (empty($signingSecret)) {
throw new Exception("No secret key");
}
//Name of hashing algorithm used by the sender- "sha256" others includes - "haval160,4" "md5", etc..
$computedSignature = hash_hmac('sha256', $request->getContent(), $signingSecret);
return hash_equals($signature, $computedSignature);
}
}
<?php
namespace App\Handler;
use Illuminate\Http\Request;
use Spatie\WebhookClient\WebhookProfile\WebhookProfile;
class ShouldProcessCallHandler implements WebhookProfile
{
/**
* @param Request $request
* @return bool
*/
public function shouldProcess(Request $request): bool
{
// you can filter out request you want to save into the DB here
if ($request->has('user')) {
return true;
}
return false;
}
}
<?php
namespace App\Handler;
use Spatie\WebhookClient\Jobs\ProcessWebhookJob;
class WebhookJobHandlerForAppTwo extends ProcessWebhookJob
{
public function handle()
{
//I have access to $this->webhookCall provided in ProcessWebhookJob class
logger($this->webhookCall);
sleep(50);
logger("I am done");
}
}
Let’s update the webhook-client to use our custom signature_validator, webhook_profile, and process_webhook_job
<?php
return [
'configs' => [
/**
* Configuation file for second application
*/
[
/*
* This package supports multiple webhook receiving endpoints. If you only have
* one endpoint receiving webhooks, you can use 'default'.
*/
'name' => 'Application-one',
/*
* We expect that every webhook call will be signed using a secret. This secret
* is used to verify that the payload has not been tampered with.
*/
'signing_secret' => env('WEBHOOK_CLIENT_SECRET_FOR_APPLICATION_ONE'),
/*
* The name of the header containing the signature.
*/
'signature_header_name' => 'Signature',
/*
* This class will verify that the content of the signature header is valid.
*
* It should implement \Spatie\WebhookClient\SignatureValidator\SignatureValidator
*/
'signature_validator' => \Spatie\WebhookClient\SignatureValidator\DefaultSignatureValidator::class,
/*
* This class determines if the webhook call should be stored and processed.
*/
'webhook_profile' => \Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile::class,
/*
* This class determines the response on a valid webhook call.
*/
'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultRespondsTo::class,
/*
* The classname of the model to be used to store webhook calls. The class should
* be equal or extend Spatie\WebhookClient\Models\WebhookCall.
*/
'webhook_model' => \Spatie\WebhookClient\Models\WebhookCall::class,
/*
* In this array, you can pass the headers that should be stored on
* the webhook call model when a webhook comes in.
*
* To store all headers, set this value to `*`.
*/
'store_headers' => [
],
/*
* The class name of the job that will process the webhook request.
*
* This should be set to a class that extends \Spatie\WebhookClient\Jobs\ProcessWebhookJob.
*/
'process_webhook_job' => \App\Handler\WebhookJobHandlerForAppOne::class,
],
/**
* Configuation file for second application
*/
[
'name' => 'Application-two',
/*
* We expect that every webhook call will be signed using a secret. This secret
* is used to verify that the payload has not been tampered with.
*/
'signing_secret' => env('WEBHOOK_CLIENT_SECRET_FOR_APPLICATION_TWO'),
/*
* The name of the header containing the signature.
*/
'signature_header_name' => 'Signature',
/*
* This class will verify that the content of the signature header is valid.
*
* It should implement \Spatie\WebhookClient\SignatureValidator\SignatureValidator
*/
'signature_validator' => \App\Handler\WebHookSignerHandlerForAppTwo::class,
/*
* This class determines if the webhook call should be stored and processed.
*/
'webhook_profile' => \App\Handler\ShouldProcessCallHandler::class,
/*
* This class determines the response on a valid webhook call.
*/
'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultRespondsTo::class,
/*
* The classname of the model to be used to store webhook calls. The class should
* be equal or extend Spatie\WebhookClient\Models\WebhookCall.
*/
'webhook_model' => \Spatie\WebhookClient\Models\WebhookCall::class,
/*
* In this array, you can pass the headers that should be stored on
* the webhook call model when a webhook comes in.
*
* To store all headers, set this value to `*`.
*/
'store_headers' => [
],
/*
* The class name of the job that will process the webhook request.
*
* This should be set to a class that extends \Spatie\WebhookClient\Jobs\ProcessWebhookJob.
*/
'process_webhook_job' => \App\Handler\WebhookJobHandlerForAppTwo::class,
],
],
];
Let’s also update our web and VerifyCsrfToken file
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
Route::webhooks('webhook-url-1', 'application-one');
Route::webhooks('webhook-url-2', 'application-two');
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
'webhook-url-1',
'webhook-url-2',
];
}
Our application is ready to receive webhook requests
Don’t forget to run php artisan queue:listen
to process the jobs.