Laravel ships with CSRF protection on every web route. Webhook providers send POST requests without session cookies or CSRF tokens, so your handler never runs — you get a 419 before the controller executes. The fix is a targeted CSRF exemption plus reading the raw body with $request->getContent() before JSON parsing.
Exempt webhook routes from CSRF
Add your webhook URI to the $except array in App\Http\Middleware\VerifyCsrfToken (or the middleware class registered in bootstrap/app.php on Laravel 11+):
protected $except = [
'webhooks/stripe',
'webhooks/*',
];
Scope exemptions narrowly. Only webhook paths that receive server-to-server POSTs belong here — never blanket-disable CSRF for your whole app.
Read the raw body for signature verification
After CSRF is out of the way, read untouched bytes:
public function handle(Request $request)
{
$payload = $request->getContent(); // raw string
$signature = $request->header('Stripe-Signature', '');
if (!$this->verifySignature($payload, $signature)) {
return response('', 401);
}
$event = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
$this->processEvent($event);
return response('', 200);
}
Use getContent(), not $request->all() or $request->input(). Laravel's input helpers assume parsed form/JSON data and will not give you the exact bytes the provider signed.
Verify signatures before business logic
Extract provider-specific headers, compute the expected digest from the raw payload, and compare with a timing-safe function (hash_equals in PHP). Return 401 on mismatch. Only after verification succeeds should you decode JSON and dispatch jobs. See the signature verification guide for provider-specific formats.
Set up the tunnel
- Start Laravel:
php artisan serve --port=8000. - In a second terminal:
npx portpreview 8000. - Paste the tunnel URL plus your webhook path into the provider dashboard.
- If you use
TrustProxies, ensure forwarded headers from the tunnel are trusted so$request->ip()and URL generation stay correct. - Trigger a test event and confirm it reaches your controller.
Common pitfalls
Reading the body twice
Calling getContent() after $request->json() or file_get_contents('php://input') elsewhere returns empty. Read once, store in a variable, verify, then parse.
Route middleware order
Global middleware that parses JSON before your controller runs can alter bytes. Keep webhook routes on the api stack or a dedicated route group without parsing side effects.
Retries and idempotency
Providers retry on slow or non-2xx responses. Persist event IDs and short-circuit duplicates — see retry and idempotency patterns.
Where to go deeper
For fundamentals, read what localhost tunneling is and how to debug webhooks locally. For signature mechanics, see the signature verification guide. For duplicate-safe handlers, read retry and idempotency patterns. start PortPreview free.
