How to handle Webhook in Laravel: Two ways and the best way

Chimeremze Prevail Ejimadu
9 min readNov 30, 2023

--

Image by the author

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.

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

  1. PaystackSignature.php
  2. 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 the VerifyCsrfToken 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

  1. 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.
  2. 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.
  3. 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

--

--

Chimeremze Prevail Ejimadu

Laravel Developer + Writer + Entrepreneur + Open source contributor + Founder + Open for projects & collaborations. Hit FOLLOW ⤵