Complete guide to webhooks, debugging, and real-time event monitoring with HookMetry
Stripe uses HMAC-SHA256 signatures in the Stripe-Signature header with a timestamp prefix to prevent replay attacks.
Critical: Use the Stripe SDK
Never manually verify Stripe signatures. Stripe's verification also checks the timestamp tolerance (5-minute window). The SDK handles this correctly; DIY implementations often get it wrong.
whsec_whsec_ secretInstall: npm install stripe
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
// Must use express.raw() — NOT express.json() — for this route
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body, // raw Buffer
sig,
process.env.STRIPE_WEBHOOK_SECRET // whsec_...
);
} catch (err) {
return res.status(400).send('Webhook error: ' + err.message);
}
switch (event.type) {
case 'payment_intent.succeeded':
const pi = event.data.object;
console.log('Payment succeeded:', pi.id, pi.amount / 100, pi.currency);
break;
case 'customer.subscription.deleted':
console.log('Subscription cancelled:', event.data.object.id);
break;
default:
console.log('Unhandled event:', event.type);
}
res.status(200).json({ received: true });
});| Event | When it fires |
|---|---|
| payment_intent.succeeded | Payment fully collected |
| payment_intent.payment_failed | Card declined or payment failed |
| customer.subscription.created | New subscription started |
| customer.subscription.deleted | Subscription cancelled or expired |
| invoice.payment_succeeded | Subscription renewal paid |
| checkout.session.completed | Checkout session finished |
express.json() globally — body is already parsed when Stripe SDK receives itsk_...) instead of the webhook signing secret (whsec_...)Was this page helpful?
Your feedback helps us improve the docs.