php
49 lines · 8 steps
Verifying Stripe webhooks in Laravel
A Laravel controller that authenticates incoming Stripe webhooks and dispatches each event type to the right handler.
Explained by
highlit
1<?php
2
3namespace App\Http\Controllers;
4
5use Illuminate\Http\Request;
6use Illuminate\Support\Facades\Log;
7use Stripe\Exception\SignatureVerificationException;
8use Stripe\Webhook;
9use Symfony\Component\HttpFoundation\Response;
10
11class StripeWebhookController extends Controller
12{
13 public function handle(Request $request): Response
14 {
15 $payload = $request->getContent();
16 $signature = $request->header('Stripe-Signature');
17 $secret = config('services.stripe.webhook_secret');
18
19 try {
20 $event = Webhook::constructEvent($payload, $signature, $secret);
21 } catch (\UnexpectedValueException $e) {
22 Log::warning('Stripe webhook: invalid payload', ['error' => $e->getMessage()]);
23
24 return response('Invalid payload', Response::HTTP_BAD_REQUEST);
25 } catch (SignatureVerificationException $e) {
26 Log::warning('Stripe webhook: signature mismatch', ['error' => $e->getMessage()]);
27
28 return response('Invalid signature', Response::HTTP_BAD_REQUEST);
29 }
30
31 match ($event->type) {
32 'checkout.session.completed' => $this->fulfillOrder($event->data->object),
33 'invoice.payment_failed' => $this->flagPastDue($event->data->object),
34 default => Log::info('Stripe webhook: unhandled event', ['type' => $event->type]),
35 };
36
37 return response()->json(['received' => true]);
38 }
39
40 protected function fulfillOrder(object $session): void
41 {
42 Order::where('checkout_session_id', $session->id)->firstOrFail()->markPaid();
43 }
44
45 protected function flagPastDue(object $invoice): void
46 {
47 Subscription::where('stripe_id', $invoice->subscription)->firstOrFail()->markPastDue();
48 }
49}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Always verify a webhook's signature against your secret before trusting its payload.
- 2A match expression cleanly routes each event type to its handler with a safe default.
- 3Return 400 on bad input so the provider retries, and 200 once you've accepted the event.
Related explainers
go
func RecoveryHandler(logger *zap.Logger) gin.HandlerFunc { return gin.CustomRecovery(func(c *gin.Context, recovered any) { var brokenPipe bool if ne, ok := recovered.(*net.OpError); ok {
A panic-recovery middleware in Gin
middleware
panic-recovery
structured-logging
Intermediate
6 steps
php
<?php namespace App\Events;
A priority event dispatcher in PHP
observer-pattern
callbacks
priority-ordering
Intermediate
8 steps
javascript
const STORAGE_KEY = "app_state"; const CURRENT_VERSION = 3; const migrations = {
Versioned state migrations in localStorage
migrations
persistence
versioning
Intermediate
9 steps
javascript
const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); };
Async error handling in Express routes
async-await
error-handling
middleware
Intermediate
7 steps
rust
use axum::{ extract::{Path, State}, http::StatusCode, routing::get,
Building a JSON user API in Axum
routing
shared-state
json-serialization
Intermediate
8 steps
python
import stripe from fastapi import APIRouter, Request, Header, HTTPException from app.config import settings
Handling Stripe webhooks in FastAPI
webhooks
signature-verification
event-routing
Intermediate
7 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/verifying-stripe-webhooks-in-laravel-explained-php-ac08/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.