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

Walkthrough

Space play step click any line
Three takeaways
  1. 1Always verify a webhook's signature against your secret before trusting its payload.
  2. 2A match expression cleanly routes each event type to its handler with a safe default.
  3. 3Return 400 on bad input so the provider retries, and 200 once you've accepted the event.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Verifying Stripe webhooks in Laravel — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code