The definitive Guide to Webhooks in Laravel

Last updated on by

The definitive Guide to Webhooks in Laravel image

These days, real-time communication isn't just expected — it's essential. Our users are done with waiting for full-page refreshes to get the latest info. Enter webhooks, the unsung heroes of the tech world. They're not widely talked about, but that's about to change. I'm on a mission to create the ultimate guide to webhooks in Laravel, making it the go-to resource for integrating webhooks in your next Laravel app.

What Are Webhooks?

Let’s jump into the vibrant world of webhooks! I like to imagine webhooks as the digital messengers of the internet, powering application to transmit real-time data to other applications, keeping them updated whenever specific events or actions occur. Whether it’s the a user registered event or a payment processed, webhooks keep you application in sync. They’re the pivotal component that ensures other applications are in perfect sync with significant events. Dive into more details with this awesome blog post.

How Webhooks Work

What kind of magic conjures up a webhook? It's just like a standard HTTP request but with a bit more pizzazz! Typically, it’s an HTTP POST request because webhooks need to send data. But here’s the real standout feature: what makes webhooks cool is the inclusion of either the X-Signature or X-Hub-Signature. This addition ensures that the payload hasn’t been meddled with, keeping your data absolutely pristine and reliable!

Boost your application’s security and guard your data like never before with HTTPS! Don’t let crafty attackers capture the sensitive information you share with clients. It’s time to upgrade and unleash the power of HTTPS!

When that webhook hits your application, it’s showtime! You’ve got to verify exactly where it’s coming from — only process webhooks from sources you recognize and trust. Generally, your webhook will arrive with a signed key. Consider it like a secret handshake. You can decode the X-Signature or X-Hub-Signature, and it should perfectly match the payload sent. If there's any inconsistency between the content and the decrypted header — stop immediately. That webhook’s been tampered with, so don’t proceed further. Let’s keep things secure!

Implementing Webhooks in Laravel

Let's dive into how we can effectively integrate webhooks into a Laravel application. Ready? Let's go! 🚀

Step 1: Define a Route

Route::post(
    'ingress/github',
    AcceptGitHubWebhooks::class,
)->name('ingress:github');

We've set up a route for ingress/github, which is going to be our powerhouse for managing all the webhooks coming in from GitHub. You can configure this in your GitHub repository settings under "Webhooks." When you're adding this webhook, you have the freedom to choose the content-type header and the signature that'll optimize your request signing. With this setup, we're ready to accept requests, supported by a robust controller to manage all the action. For an added layer of awesomeness, include some middleware around this route to enhance the verification of the source and payload.

Step 2: Add Middleware for Verification

Let's fire up an Artisan command to create some middleware that will enhance our request verification by thoroughly checking the source and content.

php artisan make:middleware VerifyGitHubWebhook

After creating this, we can update our route definition.

Route::post(
    'ingress/github',
    AcceptGitHubWebhooks::class,
)->name('ingress:github')->middleware([
    VerifyGitHubWebhook::class,
]);

Let's proceed to the middleware to explore verifying this incoming webhook.

final class VerifyGitHubWebhook
{
public function handle(Request $request, Closure $next): Response
{
if (! $this->isValidSource($request->ip())) {
return new JsonResponse(
data: ['message' => 'Invalid source IP.'],
status: 403,
);
}
 
$signature = $request->header('X-Hub-Signature-256');
$secret = config('services.github.webhook_secret');
 
if ( ! $signature || ! $this->isValidSignature(
$request->getContent(),
$signature,
$secret,
)) {
return new JsonResponse(
data: ['message' => 'Invalid signature.'],
status: 403,
);
}
 
return $next($request);
}
 
private function isValidSignature(
string $payload,
string $signature,
string $secret,
): bool {
return hash_equals(
'sha256=' . hash_hmac('sha256', $payload, $secret),
$signature
);
}
 
private function isValidSource(string $ip): bool
{
$validIps = cache()->remember(
key: 'github:webhook_ips',
ttl: 3600,
callback: fn () => Http::get(
url: 'https://api.github.com/meta',
)->json('hooks', []),
);
 
return in_array($ip, $validIps, true);
}
}

Let's dive right into our middleware. First, we’re snatching the source IP address from the request and matching it against some IP addresses we get from the GitHub API. But hey, why not make it more efficient by caching them? Just don’t forget to refresh that cache every now and then. Next, grab the signature header and fetch the signing key from our app’s config. Time to roll up our sleeves and verify that the hashed version of our request payload matches the given signature! Do this, and we’ve got solid proof that GitHub’s got our back with the webhook data, and nobody else has been tampering with the data.

Step 3: Dispatch a Job in the Controller

Let's dive into the controller code to unleash the potential of this webhook! Remember the battle cry: Verify - Queue - Respond! We've already verified the source, so what's next? It's time for our controller to dispatch a background job with the incoming payload.

final class AcceptGitHubWebhooks
{
public function __construct(private Dispatcher $bus) {}
 
public function __invoke(Request $request): Response
{
defer(
callback: fn() => $this->bus->dispatch(
command: new HandleGitHubWebhook(
payload: $request->all(),
),
),
);
 
return new JsonResponse(
data: ['message' => 'Webhook accepted.'],
status: Response::HTTP_ACCEPTED,
);
}
}

Our controller's task is to dispatch the HandleGitHubWebhook job to the queue and promptly return a response, ensuring the sender is delighted to learn that the delivery was successful. At this stage, nothing should impede your webhook workflow: should you experience a sudden influx of webhooks, your queue is prepared to manage it — or you can deploy additional workers to handle your queued jobs. We have wrapped the dispatching here in a Laravel 11 helper, that will wait until the response has been sent before dispatching this job. What I'd call a micro-optimization but a pretty nifty one.

Step 4: Process the Payload

Our controller is in excellent condition, but we are not stopping there. We're need to tackle the task of processing the incoming payload and setting things in motion. When a webhook is sent our way, it arrives as a JSON object filled with an event property. Next, we will align this event's name with our dynamic internal business logic. Let's dive into the HandleGitHubWebhook job and explore how we can make this happen.

final class HandleGitHubWebhook implements ShouldQueue
{
use Queueable;
 
public function __construct(public array $payload) {}
 
public function handle(GitHubService $service): void
{
$action = $this->payload['action'];
 
match ($action) {
'published' => $service->release(
payload: $this->payload,
),
'opened' => $service->opened(
payload: $this->payload,
),
default => Log::info(
message: "Unhandled webhook action: $action",
context: $this->payload,
),
};
}
}

We're harnessing the power of the ShouldQueue interface, signaling Laravel to give this class the special treatment it deserves. Next, we inject the Queueable trait to enhance our queue behavior. And sure, if you ever feel like living on the edge, you can override the trait's methods, but honestly, after more than 8 years diving deep into Laravel, I've never needed to. The constructor accepts the payload we've dispatched from our controller. It's assigned as a public property because when this job is serialized onto the queue, it can't regenerate with private properties being unserialized. Finally, we have our handle method, the all-important function that springs into action to tackle this job on the queue. And guess what? You can leverage dependency injection on the handle method if there's anything specific you need to manage your business logic.

What I like to do next is write a service class that will contain all of the logic for handling the webhooks for each source. Let's work on a service under app/Services/GitHubService.

final class GitHubService
{
public function __construct(private DatabaseManager $database) {}
 
public function release(array $payload): void
{
$this->database->transaction(function () use ($payload) {
// Handle the Release Published logic here
});
}
 
public function opened(array $payload): void
{
$this->database->transaction(function () use ($payload) {
// Handle the PR opened logic here
});
}
}

Defining a specific method for each webhook action we aim to accept maintains everything orderly within the service class. We're now managing all desired GitHub webhooks, making it a breeze to expand as needed — without impacting our core business logic.

Overview

It is important to bear in mind that while we only covered GitHub webhooks in this article, the same principles can be applied to any webhook that you want to ingest. The process is capture the request in our router, and route this through to our controller through some middleware. The middleware will validate the source and payload, returning an acceptable error where needed, finally passing this through to the controller. The controllers job is to pass this webhook data to the background as quickly as possible, so that there is no delay in responding from the request. Once the response has been sent, we can process the webhook on a queue worker in the background, so our main application is free to accept any additional requests without it effecting our webhook processing.

But there's a twist! How do we ensure we're catching all the webhooks we're supposed to? How do we make sure GitHub retries sending webhooks if the first attempt stumbles for some reason? We've got zero observability, unaware of what might be slipping through our fingers - or what our error ratio looks like. We're not alerted and remain in the dark about any failures - and that really amps up my developer spidey sense!

Observability and Resilience

Hookdeck is your ultimate solution for managing webhooks with flair! Configure, monitor, and observe all your webhooks seamlessly in one powerful platform. No more juggling multiple sources - Hookdeck simplifies it in style. Need to reset the signature or retry a webhook? Do it effortlessly, with fantastic filters and super-smart routing built in. Say goodbye to messy logic and hello to determining which webhooks you want.

With Hookdeck, you no longer have to handle this in the background because it’s your ultimate webhook queue! It sends and retries webhook deliveries, hitting multiple targets like your API and AWS S3. Hookdeck verifies each webhook’s source and content for you, so all you need to do is sit back, accept that HTTP request, maybe grab a coffee, and let the magic happen!

Steve McDougall photo

Educator and Content creator, freelance consultant, API evangelist

Filed in:
Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
Cut PHP Code Review Time & Bugs into Half with CodeRabbit logo

Cut PHP Code Review Time & Bugs into Half with CodeRabbit

CodeRabbit is an AI-powered code review tool that specializes in PHP and Laravel, running PHPStan and offering automated PR analysis, security checks, and custom review features while remaining free for open-source projects.

Cut PHP Code Review Time & Bugs into Half with CodeRabbit
Join the Mastering Laravel community logo

Join the Mastering Laravel community

Connect with experienced developers in a friendly, noise-free environment. Get insights, share ideas, and find support for your coding challenges. Join us today and elevate your Laravel skills!

Join the Mastering Laravel community
Laravel Idea for PhpStorm logo

Laravel Idea for PhpStorm

Ultimate PhpStorm plugin for Laravel developers, delivering lightning-fast code completion, intelligent navigation, and powerful generation tools to supercharge productivity.

Laravel Idea for PhpStorm
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Handling Request Data Presence in Laravel image

Handling Request Data Presence in Laravel

Read article
First Factor One-Time Passwords for Laravel with OTPZ image

First Factor One-Time Passwords for Laravel with OTPZ

Read article
Managing Request Host Information in Laravel image

Managing Request Host Information in Laravel

Read article
Tim Leland: URL Shorteners, browser extensions, and more image

Tim Leland: URL Shorteners, browser extensions, and more

Read article
HTTP Method Verification in Laravel image

HTTP Method Verification in Laravel

Read article
Packistry is a Self-hosted Composer Repository Made with Laravel image

Packistry is a Self-hosted Composer Repository Made with Laravel

Read article