Learn how to fix Stripe webhook "invalid signature" errors by using the correct secret, verifying the raw request body, and configuring your server properly.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
How to Fix Stripe Webhook "Invalid Signature" Error: A Comprehensive Tutorial
Step 1: Understand What Causes the "Invalid Signature" Error
The "invalid signature" error occurs when Stripe cannot validate the webhook signature. This typically happens for several reasons:
Step 2: Check Your Webhook Secret
Ensure you're using the correct webhook secret. This is unique for each webhook endpoint you create in the Stripe dashboard.
Step 3: Properly Configure Your Server to Handle Raw Request Bodies
In many frameworks, the request body is automatically parsed, which can cause signature verification to fail. You need to ensure you're verifying the signature against the raw request body.
For Express.js, use the raw body parser before any other middleware:
const express = require('express');
const stripe = require('stripe')('sk_test_...');
const bodyParser = require('body-parser');
const app = express();
// This must come BEFORE any other bodyParser middleware
app.use('/webhook', bodyParser.raw({type: 'application/json'}));
// Other middleware and routes
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.post('/webhook', (req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.body, // This should be the raw body
sig,
'whsec\_...' // Your webhook secret
);
// Handle the event
console.log('Webhook received:', event.type);
res.json({received: true});
} catch (err) {
console.error(`Webhook Error: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
});
app.listen(3000, () => console.log('Running on port 3000'));
Step 4: Framework-Specific Configurations
Different frameworks require different approaches:
For Ruby on Rails:
# In config/routes.rb
post "/webhooks/stripe" => "webhooks#stripe"
# In app/controllers/webhooks\_controller.rb
class WebhooksController < ApplicationController
# Skip CSRF protection for webhooks
skip_before_action :verify_authenticity_token
def stripe
payload = request.body.read
sig_header = request.env['HTTP_STRIPE\_SIGNATURE']
endpoint_secret = ENV['STRIPE_WEBHOOK\_SECRET']
begin
event = Stripe::Webhook.construct\_event(
payload, sig_header, endpoint_secret
)
rescue JSON::ParserError => e
render json: {error: 'Invalid payload'}, status: 400
return
rescue Stripe::SignatureVerificationError => e
render json: {error: 'Invalid signature'}, status: 400
return
end
# Handle the event
puts "Webhook received: #{event.type}"
render json: {received: true}
end
end
For Django:
# views.py
import json
import stripe
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf\_exempt
@csrf\_exempt
def stripe\_webhook(request):
payload = request.body
sig_header = request.META['HTTP_STRIPE\_SIGNATURE']
endpoint_secret = 'whsec_...' # Your webhook secret
try:
event = stripe.Webhook.construct\_event(
payload, sig_header, endpoint_secret
)
except ValueError as e:
# Invalid payload
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return HttpResponse(status=400)
# Handle the event
print('Webhook received:', event['type'])
return JsonResponse({'received': True})
For Laravel:
// routes/web.php
Route::post('/webhook/stripe', 'WebhookController@handleStripeWebhook');
// app/Http/Controllers/WebhookController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Stripe\Webhook;
use Stripe\Exception\SignatureVerificationException;
class WebhookController extends Controller
{
public function \_\_construct()
{
// Disable CSRF for the webhook route
$this->middleware('stripe.webhook');
}
public function handleStripeWebhook(Request $request)
{
$payload = $request->getContent();
$sigHeader = $request->header('Stripe-Signature');
$endpointSecret = env('STRIPE_WEBHOOK_SECRET');
try {
$event = Webhook::constructEvent(
$payload, $sigHeader, $endpointSecret
);
} catch (SignatureVerificationException $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
// Handle the event
\Log::info('Webhook received: ' . $event->type);
return response()->json(['received' => true]);
}
}
// Create a middleware for Stripe webhooks in app/Http/Middleware/StripeWebhookMiddleware.php
namespace App\Http\Middleware;
use Closure;
class StripeWebhookMiddleware
{
public function handle($request, Closure $next)
{
// Don't process the content types we don't need
if ($request->isJson()) {
$request->headers->set('Content-Type', 'application/json');
}
return $next($request);
}
}
// Register the middleware in app/Http/Kernel.php
protected $routeMiddleware = [
// Other middleware...
'stripe.webhook' => \App\Http\Middleware\StripeWebhookMiddleware::class,
];
Step 5: Check for Multiple Body Readings
Make sure the webhook payload is not being read multiple times before verification. For example, logging the request body before verifying the signature can cause issues.
// INCORRECT:
app.post('/webhook', (req, res) => {
const body = req.body;
console.log('Request body:', JSON.stringify(body)); // This consumes the stream
const sig = req.headers['stripe-signature'];
try {
// This will fail because the body was already consumed
const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
// ...
} catch (err) {
// ...
}
});
// CORRECT:
app.post('/webhook', (req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
console.log('Event data:', JSON.stringify(event.data)); // Log after verification
// ...
} catch (err) {
// ...
}
});
Step 6: Handle Potential Clock Skew
Stripe's webhooks include a timestamp in the signature and will reject events that are too old. Ensure your server's clock is synchronized correctly.
// You can increase the tolerance if needed (default is 300 seconds)
const event = stripe.webhooks.constructEvent(
payload,
signature,
endpointSecret,
60 \* 10 // 10 minutes tolerance (use only if necessary)
);
Step 7: Use the Stripe CLI for Local Testing
When developing locally, use the Stripe CLI to forward webhook events to your local server:
Install the Stripe CLI: https://stripe.com/docs/stripe-cli
Login to your Stripe account:
stripe login
Forward events to your local server:
stripe listen --forward-to http://localhost:3000/webhook
The CLI will provide you with a webhook signing secret to use in your code. Use this temporary secret for local development.
Step 8: Implement Logging for Debugging
Add detailed logging to help debug webhook issues:
app.post('/webhook', (req, res) => {
const sig = req.headers['stripe-signature'];
console.log('Received webhook with signature:', sig);
try {
const event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
console.log('Successfully verified webhook signature');
console.log('Event type:', event.type);
// Handle the event
res.json({received: true});
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
console.error('Webhook secret used:', webhookSecret.substring(0, 5) + '...');
return res.status(400).send(`Webhook Error: ${err.message}`);
}
});
Step 9: Verify in Stripe Dashboard
Check the Stripe Dashboard to see if your webhook endpoint is receiving events correctly:
Step 10: Handling Proxy Servers or Load Balancers
If your application is behind a proxy server or load balancer, the request body might be modified before it reaches your application. Ensure that the raw body is preserved:
// For Express.js behind a proxy
app.set('trust proxy', true);
// Ensure your proxy or load balancer doesn't modify the request body
// For Nginx, add this to your config:
// proxy_set_header Content-Type $http_content_type;
// proxy_pass_request\_body on;
Step 11: Test with a Valid Webhook Payload
Create a test endpoint to manually verify webhook signatures:
// A test endpoint to manually verify webhook signatures
app.post('/test-webhook', bodyParser.raw({type: 'application/json'}), (req, res) => {
const payload = req.body;
const payloadString = payload.toString();
const signature = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET;
console.log('Payload:', payloadString);
console.log('Signature:', signature);
console.log('Secret:', secret);
try {
const event = stripe.webhooks.constructEvent(payload, signature, secret);
console.log('Verification successful!');
console.log('Event:', event);
return res.json({success: true, event: event});
} catch (err) {
console.error('Verification failed:', err.message);
return res.status(400).json({error: err.message});
}
});
Conclusion
By following these steps, you should be able to resolve most "invalid signature" errors when working with Stripe webhooks. Remember that the key points are:
If you're still experiencing issues after following these steps, contact Stripe support for further assistance.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.