Verify Stripe Webhook Signature

Paste your payload, signature header, and signing secret to verify. Get detailed diagnostics if verification fails.

🔒 Your signing secret never leaves your browser. All verification happens client-side.

Find this in your Stripe Dashboard → Developers → Webhooks

Code Examples

Copy-paste examples for verifying Stripe webhook signatures in your application.

Node.js / Express

Using official Stripe SDK
const express = require('express');
        const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

        const app = express();

        // IMPORTANT: Use raw body for webhook routes
        app.post('/webhook', 
        express.raw({ type: 'application/json' }), 
        (req, res) => {
            const sig = req.headers['stripe-signature'];
            const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
            
            let event;
            
            try {
            event = stripe.webhooks.constructEvent(
                req.body,  // Raw buffer
                sig,
                endpointSecret
            );
            } catch (err) {
            console.log(`Webhook Error: ${err.message}`);
            return res.status(400).send(`Webhook Error: ${err.message}`);
            }
            
            // Handle the event
            switch (event.type) {
            case 'payment_intent.succeeded':
                const paymentIntent = event.data.object;
                console.log('PaymentIntent succeeded:', paymentIntent.id);
                break;
            default:
                console.log(`Unhandled event type ${event.type}`);
            }
            
            res.json({ received: true });
        }
        );

        app.listen(3000);

Next.js App Router

Route handler
// app/api/webhook/route.ts
        import { NextRequest, NextResponse } from 'next/server';
        import Stripe from 'stripe';

        const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
        apiVersion: '2023-10-16',
        });

        export async function POST(req: NextRequest) {
        const body = await req.text();
        const sig = req.headers.get('stripe-signature')!;
        const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
        
        let event: Stripe.Event;
        
        try {
            event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
        } catch (err) {
            console.error('Webhook signature verification failed:', err);
            return NextResponse.json(
            { error: 'Invalid signature' },
            { status: 400 }
            );
        }
        
        // Handle the event
        switch (event.type) {
            case 'payment_intent.succeeded':
            const paymentIntent = event.data.object;
            console.log('PaymentIntent succeeded:', paymentIntent.id);
            break;
            default:
            console.log(`Unhandled event type: ${event.type}`);
        }
        
        return NextResponse.json({ received: true });
        }

Python / Flask

Using official Stripe SDK
import stripe
        from flask import Flask, request, jsonify
        import os

        app = Flask(__name__)
        stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
        endpoint_secret = os.getenv('STRIPE_WEBHOOK_SECRET')

        @app.route('/webhook', methods=['POST'])
        def webhook():
            payload = request.get_data(as_text=True)
            sig_header = request.headers.get('Stripe-Signature')
            
            try:
                event = stripe.Webhook.construct_event(
                    payload, sig_header, endpoint_secret
                )
            except ValueError as e:
                # Invalid payload
                return jsonify({'error': str(e)}), 400
            except stripe.error.SignatureVerificationError as e:
                # Invalid signature
                return jsonify({'error': str(e)}), 400
            
            # Handle the event
            if event['type'] == 'payment_intent.succeeded':
                payment_intent = event['data']['object']
                print(f"PaymentIntent succeeded: {payment_intent['id']}")
            else:
                print(f"Unhandled event type: {event['type']}")
            
            return jsonify({'success': True}), 200

        if __name__ == '__main__':
            app.run(port=3000)

Go

Using official Stripe SDK
package main

        import (
            "encoding/json"
            "io"
            "log"
            "net/http"
            "os"
            
            "github.com/stripe/stripe-go/v76"
            "github.com/stripe/stripe-go/v76/webhook"
        )

        func main() {
            stripe.Key = os.Getenv("STRIPE_SECRET_KEY")
            
            http.HandleFunc("/webhook", handleWebhook)
            log.Fatal(http.ListenAndServe(":3000", nil))
        }

        func handleWebhook(w http.ResponseWriter, r *http.Request) {
            const MaxBodyBytes = int64(65536)
            r.Body = http.MaxBytesReader(w, r.Body, MaxBodyBytes)
            
            payload, err := io.ReadAll(r.Body)
            if err != nil {
                http.Error(w, "Error reading body", http.StatusBadRequest)
                return
            }
            
            endpointSecret := os.Getenv("STRIPE_WEBHOOK_SECRET")
            signatureHeader := r.Header.Get("Stripe-Signature")
            
            event, err := webhook.ConstructEvent(
                payload,
                signatureHeader,
                endpointSecret,
            )
            if err != nil {
                log.Printf("Webhook signature verification failed: %v", err)
                http.Error(w, "Invalid signature", http.StatusBadRequest)
                return
            }
            
            // Handle the event
            switch event.Type {
            case "payment_intent.succeeded":
                var paymentIntent stripe.PaymentIntent
                err := json.Unmarshal(event.Data.Raw, &paymentIntent)
                if err != nil {
                    log.Printf("Error parsing webhook JSON: %v", err)
                    http.Error(w, "Error parsing JSON", http.StatusBadRequest)
                    return
                }
                log.Printf("PaymentIntent succeeded: %s", paymentIntent.ID)
            default:
                log.Printf("Unhandled event type: %s", event.Type)
            }
            
            w.WriteHeader(http.StatusOK)
        }

Ruby / Rails

Using official Stripe SDK
# config/routes.rb
        post '/webhook', to: 'webhooks#stripe'

        # app/controllers/webhooks_controller.rb
        class WebhooksController < ApplicationController
        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
            case event.type
            when 'payment_intent.succeeded'
            payment_intent = event.data.object
            puts "PaymentIntent succeeded: #{payment_intent.id}"
            else
            puts "Unhandled event type: #{event.type}"
            end
            
            render json: { success: true }, status: 200
        end
        end

💡 Pro tip: Always use the official Stripe SDK for your language. It handles signature verification, timestamp checking, and version compatibility automatically.

Frequently Asked Questions (FAQ)

Why is my Stripe webhook signature failing?

The most common reasons for signature verification failures are:

  • Wrong signing secret: Make sure you're using the correct secret from your Stripe Dashboard
  • Test vs Live mode mismatch: Test webhooks require test mode secrets (whsec_test_...)
  • Modified request body: Your framework may have parsed the JSON before verification
  • Timestamp too old: Webhook took longer than 5 minutes to reach your server
  • Wrong endpoint secret: Each webhook endpoint has its own unique secret

Where do I find my Stripe webhook signing secret?

To find your signing secret:

  1. Go to your Stripe Dashboard → Webhooks
  2. Click on the specific webhook endpoint you want to verify
  3. Scroll down to the "Signing secret" section
  4. Click "Reveal" to show the secret (starts with whsec_)
  5. Copy the entire secret including the whsec_ prefix

Note: Test mode and live mode have different secrets. Make sure you're using the right one!

What does "timestamp too old" mean?

Stripe includes a timestamp in the signature to prevent replay attacks. Webhooks older than 5 minutes (300 seconds) are automatically rejected for security reasons.

This error usually happens when:

  • Your server's clock is out of sync
  • Network delays or slow processing
  • You're testing with old webhook payloads
  • Your server was down and is processing queued webhooks

Fix: Make sure your server's time is synchronized using NTP, or increase the tolerance window in your verification code (not recommended for production).

Why does verification work here but fail in my code?

This is almost always caused by your framework modifying the request body before you verify it. Stripe signatures must be verified against the exact raw bytes received from Stripe.

Common mistakes:

  • Parsing JSON first: Don't use req.json() before verifying
  • Body parser middleware: Disable it for webhook routes (Express, Next.js, etc.)
  • String encoding issues: Converting bytes → string → bytes can change the data
  • Whitespace normalization: Some frameworks trim or format JSON

Solution: Access the raw request body as a buffer/bytes before any parsing happens.

What's the difference between Shopify, Stripe, and GitHub signatures?

FeatureShopifyStripeGitHub
HeaderX-Shopify-Hmac-SHA256Stripe-SignatureX-Hub-Signature-256
EncodingBase64HexHex
AlgorithmHMAC-SHA256HMAC-SHA256HMAC-SHA256
TimestampNoYes (5 min tolerance)No

How do I test Stripe webhooks locally?

The best way to test Stripe webhooks locally:

  1. Install the Stripe CLI
  2. Run stripe listen --forward-to localhost:3000/webhook
  3. Copy the webhook signing secret the CLI displays
  4. Use that secret in your local environment
  5. Trigger test events with stripe trigger payment_intent.succeeded

The Stripe CLI automatically generates valid signatures for local testing.

Is it safe to verify signatures client-side?

Yes, for debugging only. HookInbox performs verification entirely in your browser using the Web Crypto API. Your signing secret never leaves your device or gets sent to our servers.

However, for production webhooks:

  • Always verify on your server - Never trust client-side verification for real webhooks
  • Keep secrets server-side - Never expose signing secrets in client code
  • Use HTTPS - Ensure your webhook endpoint uses SSL/TLS

This tool is for debugging and learning how Stripe signatures work, not for production verification.