How to handle Webhook in Laravel: Two ways and the best way
Handling a webhook request can be daunting at first. I want to show you how you can easily handle webhook requests in laravel. One of these methods is my favorite and is what I’ll recommend too.
Let’s deal with the technical terms first.
What is a webhook?
A webhook is a user-defined HTTP callback. When a predefined event occurs, the source application makes an HTTP request to the specified URL, triggering an action in the receiving application.
Simplified, a webhook is way of notifying an external system that an event has happened in your application. An application sends an HTTP request to another application whenever an event occurs.
Your application becomes the receiver while the other application is the sender (where the event was initialized).
A pretty example is when your customer makes payments via a payment gateway like Bani or Paystack. They would send a webhook request to your server with details of the payment. You listen to the webhook and when the data comes in you, verify that it’s actually from paystack and then render service to your customer. You can do things like creating an order, giving rewards or cashback, sending a “Thank you” mail and many other things.
So webhooks are important. Yes, very important.
How do we handle webhook in Laravel?
Method 1: Manual Route Handling
1. Route Definition
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
// routes/web.php
use App\Http\Controllers\WebhookController;
Route::post('webhook/endpoint', [WebhookController::class, 'handle']);
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.php
/*** The URIs that should be excluded from CSRF verification.*
* @var array<int, string>
*/
protected $except = ['webhook/endpoint'];
2. Create a Controller
Generate a controller using the Artisan command to handle the webhook logic.
php artisan make:controller WebhookController
This command creates a WebhookController.php
file in the app/Http/Controllers
directory.
3. Implement Webhook Logic
Open the WebhookController
and implement the logic to process the incoming webhook payload.
// app/Http/Controllers/WebhookController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class WebhookController extends Controller
{
public function handle(Request $request)
{
// Process webhook payload
// Perform actions based on the webhook data
return response()->json(['success' => true]);
}
}
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(['success' => true])
That’s it?
Yes, we have handled webhook. BUT THIS METHOD IS NOT RECOMMENDED.
This will work, but what if the logic you will be performing each time are heavy actions and requires a lot of CPU processes — for example, sending an email or looping through thousands of records. Then what is this webhook receives responses every minute? Then you’ll have a big problem.
Method 2 solves this problem for you.
Method 2: Queue-Based Processing
We can modify our manual method to use Jobs to handle the webhook data processing.
Queue-based processing involves the use of Laravel’s queue system to handle tasks in the background, decoupling the execution of time-consuming or resource-intensive processes from the main application flow.
- Create a Job.
Generate a job using the Artisan command to handle the webhook logic.
php artisan make:job ProcessWebhookJob
2. Implement the Job Logic
Open the generated job (ProcessWebhookJob.php
) and implement the logic to process the webhook payload.
// app/Jobs/ProcessWebhookJob.php
namespace App\Jobs;
class ProcessWebhookJob implements ShouldQueue
{
public function handle(array $payload)
{
// Process the webhook payload asynchronously
// Perform actions based on the webhook data
}
}
3. Dispatch From the webhook controller
What we do here is to go and modify the webhookController
we created in method 1 to send the payload to the the Job for queuing instead of handling it directly.
// app/Http/Controllers/WebhookController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class WebhookController extends Controller
{
public function handle(Request $request)
{
// verify the payload authenticity
// Send payload to job for processing
ProcessWebhookJob::dispatch($request->all());
return response()->json(['success' => true]);
}
}
Now, with this method, you can be sure that resource intensive tasks would not make a mess of your application.
If you do not really understand how jobs really work o r when to use them, check out this guide I wrote about Actions, Services, Events, Listeners, Observers, Traits and Jobs in Laravel.
But with this method, you still have to verify the authenticity of the incoming webhook since most webhooks are signed to improve security.
This is where the third and best way comes in.
Method 3: Laravel Webhook Packages
Particularly, Laravel-webhook-client.
This package has support for verifying signed calls, storing payloads, and processing the payloads in a queue job immediately or later.
Installation and Configuration
Let’s get started by installing the Spatie Laravel Webhook Client into your Laravel project. Open a terminal and run the following Composer command:
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:
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' => '',
],
],
/*
* The integer amount of days after which models should be deleted.
*
* 7 deletes all records after 1 week. Set to null if no models should be deleted.
*/
'delete_after_days' => 30,
];
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
We are going to use this package to handle webhook requests from paystack.
Let’s update our webhook-client.php
as below.
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('PAYSTACK_SECRET_KEY'),
/*
* The name of the header containing the signature.
*/
'signature_header_name' => 'x-paystack-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,
'signature_validator' => App\Handler\PaystackSignature::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\ProcessWebhook::class,
],
],
/*
* The integer amount of days after which models should be deleted.
*
* 7 deletes all records after 1 week. Set to null if no models should be deleted.
*/
'delete_after_days' => 30,
];
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
. You can read more about Laravel queue here
Next Step? Create the Handlers
The next thing we do is to create a folder named Handler inside the app folder. Then inside this app/Handler, create two files which are
- PaystackSignature.php
- ProcessWebhook.php
Inside app/Handler/PaystackSignature.php
, what we want to do is to validate that the request came from Paystack. Add the code to that file.
<?php
namespace App\Handler;
use Illuminate\Http\Request;
use Spatie\WebhookClient\Exceptions\WebhookFailed;
use Spatie\WebhookClient\WebhookConfig;
use Spatie\WebhookClient\SignatureValidator\SignatureValidator;
class PaystackSignature implements SignatureValidator
{
public function isValid(Request $request, WebhookConfig $config): bool
{
$signature = $request->header($config->signatureHeaderName);
if (!$signature) {
return false;
}
$signingSecret = $config->signingSecret;
if (empty($signingSecret)) {
throw WebhookFailed::signingSecretNotSet();
}
$computedSignature = hash_hmac('sha512', $request->getContent(), $signingSecret);
return hash_equals($signature, $computedSignature);
}
}
Great. So the other file app/Handler/ProcessWebhook.php extends the ProcessWebhookJob class which holds the WebhookCall variables containing each job’s detail.
<?php
namespace App\Handler;
use Illuminate\Support\Facades\Log;
use Spatie\WebhookClient\Jobs\ProcessWebhookJob;
//The class extends "ProcessWebhookJob" class as that is the class
//that will handle the job of processing our webhook before we have
//access to it.
class ProcessWebhook extends ProcessWebhookJob
{
public function handle()
{
$dat = json_decode($this->webhookCall, true);
$data = $dat['payload'];
if ($data['event'] == 'charge.success') {
// take action since the charge was success
// Create order
// Sed email
// Whatever you want
Log::info($data);
}
//Acknowledge you received the response
http_response_code(200);
}
}
The next step is to set up about route file to accept the webhook call.
Hence, add the below line of code to your web.php
Route::webhooks('paystack/webhook');
The
webhooks
method is provided by the Spatie package and sets up the necessary routes for webhook handling.Do not forget to add the endpoint to the
except
array of theVerifyCsrfToken
middleware.
PS: Don’t forget to run php artisan queue:listen
to process the jobs.
Our application is ready to receive webhook requests.
I will gift you for reading this far by dropping some security tips to consider when using webhooks in Laravel with any of the methods I listed above.
Security Tips to Consider When Using Webhooks
- Rate Limiting: Implement rate limiting for your webhook endpoints to prevent abuse or denial-of-service attacks. Laravel provides rate limiting functionality out of the box.
- IP Whitelisting: If possible, restrict incoming webhook requests to specific IP addresses by implementing IP whitelisting. This adds an extra layer of security by allowing requests only from trusted sources.
- Error Handling: Implement proper error handling for webhook requests. Avoid exposing sensitive information in error responses that could be exploited by attackers.
Conclusion
My final recommendation is that you should use the Webhook package by Spatie. It has robust features and can be used for multiple webhooks. Do well to check out their official documentation to see what more you can do with it.
Stay tuned!!! I will be back with some more cool Laravel tutorials in the next article. I hope you liked the article. Don’t forget to follow me 😇 and give some clap 👏. And if you have any questions feel free to comment.
Thank you.
Thanks a lot for reading till end. Follow or contact me via:
Twitter: https://twitter.com/EjimaduPrevail
Email: prevailexcellent@gmail.com
Github: https://github.com/PrevailExcel
LinkedIn: https://www.linkedin.com/in/chimeremeze-prevail-ejimadu-3a3535219
BuyMeCoffee: https://www.buymeacoffee.com/prevail
Chimeremeze Prevail Ejimadu