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
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
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.