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 SDKconst 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 SDKimport 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 SDKpackage 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:
- Go to your Stripe Dashboard → Webhooks
- Click on the specific webhook endpoint you want to verify
- Scroll down to the "Signing secret" section
- Click "Reveal" to show the secret (starts with
whsec_) - 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?
| Feature | Shopify | Stripe | GitHub |
|---|---|---|---|
| Header | X-Shopify-Hmac-SHA256 | Stripe-Signature | X-Hub-Signature-256 |
| Encoding | Base64 | Hex | Hex |
| Algorithm | HMAC-SHA256 | HMAC-SHA256 | HMAC-SHA256 |
| Timestamp | No | Yes (5 min tolerance) | No |
How do I test Stripe webhooks locally?
The best way to test Stripe webhooks locally:
- Install the Stripe CLI
- Run
stripe listen --forward-to localhost:3000/webhook - Copy the webhook signing secret the CLI displays
- Use that secret in your local environment
- 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.