/stripe-guides

How to fix Stripe webhook “invalid signature” error?

Learn how to fix Stripe webhook "invalid signature" errors by using the correct secret, verifying the raw request body, and configuring your server properly.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to fix Stripe webhook “invalid signature” error?

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:

  • Using the wrong webhook secret
  • Modifying the webhook payload before verification
  • Request body is not being properly parsed or is being read multiple times
  • Clock skew between your server and Stripe's servers
  • Incorrect implementation of the signature verification process

 

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.

  1. Go to the Stripe Dashboard (https://dashboard.stripe.com/)
  2. Navigate to Developers → Webhooks
  3. Select your webhook endpoint
  4. Click "Reveal" next to "Signing secret" to view your webhook secret
  5. Copy this secret and make sure it matches what you're using in your code

 

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:

  1. Install the Stripe CLI: https://stripe.com/docs/stripe-cli

  2. Login to your Stripe account:

    
    stripe login
    
  3. Forward events to your local server:

    
    stripe listen --forward-to http://localhost:3000/webhook
    
  4. 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:

  1. Go to Developers → Webhooks in the Stripe Dashboard
  2. Select your webhook endpoint
  3. Click on "Events" to see recent webhook attempts
  4. Look for any failed events and check the error message

 

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:

  • Use the correct webhook secret
  • Ensure you're verifying the signature against the raw request body
  • Configure your framework correctly to handle webhook requests
  • Don't read the request body before verification
  • Make sure your server's clock is synchronized

If you're still experiencing issues after following these steps, contact Stripe support for further assistance.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022