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
rust
use std::collections::HashMap; #[derive(Debug)] pub struct RequestHead {
Parsing an HTTP request head in Rust
parsing
error-handling
iterators
Intermediate
9 steps
php
<?php final class MarkdownParser {
Building a small Markdown-to-HTML parser in PHP
state-machine
parsing
closures
Intermediate
10 steps
go
package humanize import ( "fmt"
Parsing human-readable byte sizes in Go
parsing
regex
lookup-table
Intermediate
8 steps
php
<?php namespace App\Console\Commands;
Releasing stale document locks in Laravel
artisan-command
transactions
row-locking
Intermediate
6 steps
ruby
class TemplateInterpolator PLACEHOLDER = /\{\{\s*([\w.]+)\s*\}\}/ def initialize(strict: false)
Interpolating templates with dotted keys in Ruby
regex
string-interpolation
hash-traversal
Intermediate
6 steps
php
<?php namespace App\Providers;
Subdomain multi-tenancy routing in Laravel
multi-tenancy
service-container
route-binding
Advanced
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.