php
45 lines · 7 steps
Handling Stripe webhooks in Laravel
A controller verifies Stripe's signature, then routes each event type to the right subscription action.
Explained by
highlit
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Services\SubscriptionService;
6use Illuminate\Http\Request;
7use Illuminate\Support\Facades\Log;
8use Stripe\Exception\SignatureVerificationException;
9use Stripe\Webhook;
10use Symfony\Component\HttpFoundation\Response;
11
12class StripeWebhookController extends Controller
13{
14 public function __construct(private readonly SubscriptionService $subscriptions)
15 {
16 }
17
18 public function handle(Request $request): Response
19 {
20 try {
21 $event = Webhook::constructEvent(
22 $request->getContent(),
23 $request->header('Stripe-Signature', ''),
24 config('services.stripe.webhook_secret'),
25 );
26 } catch (SignatureVerificationException $e) {
27 Log::warning('Rejected Stripe webhook with invalid signature', ['error' => $e->getMessage()]);
28
29 return response()->json(['error' => 'invalid signature'], 400);
30 }
31
32 $object = $event->data->object;
33
34 match ($event->type) {
35 'customer.subscription.created',
36 'customer.subscription.updated' => $this->subscriptions->syncFromStripe($object),
37 'customer.subscription.deleted' => $this->subscriptions->markCanceled($object->id),
38 'invoice.payment_succeeded' => $this->subscriptions->recordPayment($object),
39 'invoice.payment_failed' => $this->subscriptions->flagPastDue($object),
40 default => Log::info('Unhandled Stripe event', ['type' => $event->type]),
41 };
42
43 return response()->json(['received' => true]);
44 }
45}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Always verify a webhook's cryptographic signature before trusting its payload.
- 2A match expression cleanly dispatches each event type to a dedicated handler.
- 3Return 400 on bad signatures but 200 once accepted, so the sender stops retrying.
Related explainers
php
<?php declare(strict_types=1);
Normalizing human names in PHP
unicode
text-normalization
transliteration
Intermediate
8 steps
typescript
import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, timer, throwError } from 'rxjs'; import { switchMap, takeWhile, filter, take, catchError } from 'rxjs/operators';
Polling a job until it finishes in Angular
rxjs
polling
observables
Intermediate
7 steps
php
<?php namespace App\FeatureFlags;
How a feature flag evaluator decides
feature-flags
rollout
hashing
Intermediate
8 steps
typescript
import { Injectable, signal, computed, effect, inject } from '@angular/core'; import { DOCUMENT } from '@angular/common'; export type Theme = 'light' | 'dark';
A signal-based theme service in Angular
signals
reactivity
dependency-injection
Intermediate
7 steps
php
namespace App\Providers; use Illuminate\Support\Facades\Blade; use Illuminate\Support\ServiceProvider;
Building a @money Blade directive in Laravel
blade-directive
service-provider
localization
Intermediate
5 steps
php
<?php namespace App\Queue;
Deduplicating a job queue with Redis
deduplication
redis
atomicity
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/handling-stripe-webhooks-in-laravel-explained-php-ca27/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.