Stripe Webhooks Integration Example: Handle Payments with Signature Verification
A Stripe webhooks integration lets your application receive real-time notifications when events happen in your Stripe account — payments succeed, subscriptions renew, disputes are opened, and more. Instead of polling the Stripe API, webhooks push real-time event data to your endpoint as a JSON payload.

The problem? Setting up Stripe webhooks correctly takes time. You need to verify signatures, handle retries, manage event ordering, and ensure reliability. Building custom retry logic alone can take days.
The solution? Stop building custom retry logic—Codehooks handles webhook reliability at the platform level. Deploy a production-ready Stripe webhook handler in under 5 minutes using our ready-made template, with automatic retries and signature verification built in.
If you're new to webhooks, check out our guide on What are webhooks? to understand the fundamentals before diving into this Stripe-specific implementation.
Prerequisites
Before you begin, sign up for a free Codehooks.io account and install the CLI:
npm install -g codehooks
Quick Deploy with Template
The fastest way to get started is using the Codehooks Stripe webhook template:
coho create mystripehandler --template webhook-stripe-minimal
You'll see output like:
Creating project: mystripehandler
Project created successfully!
Your API endpoints:
https://mystripehandler-a1b2.api.codehooks.io/dev
Then install, deploy, and configure:
cd mystripehandler
npm install
coho deploy
coho set-env STRIPE_SECRET_KEY sk_xxxxx --encrypted
coho set-env STRIPE_WEBHOOK_SECRET whsec_xxxxx --encrypted
That's it! The template includes signature verification, event handling, and database storage out of the box.
The webhook-stripe-minimal template is production-ready with proper signature verification, error handling, and event storage. Browse all available templates in the templates repository.
Custom Implementation
If you prefer to build from scratch or customize further, create a new project and add your Stripe webhook handler code to index.js:
import { app } from 'codehooks-js';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Stripe webhook endpoint with signature verification
app.post('/stripe/webhooks', async (req, res) => {
const sig = req.headers['stripe-signature'];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
// Verify the webhook signature
event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
endpointSecret
);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).json({ error: 'Invalid signature' });
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('Payment succeeded:', paymentIntent.id);
// Update your database, send confirmation email, etc.
break;
case 'invoice.paid':
const invoice = event.data.object;
console.log('Invoice paid:', invoice.id);
// Extend subscription, update records
break;
case 'customer.subscription.created':
const subscription = event.data.object;
console.log('New subscription:', subscription.id);
// Provision access, welcome email
break;
case 'customer.subscription.deleted':
const canceledSub = event.data.object;
console.log('Subscription canceled:', canceledSub.id);
// Revoke access, send win-back email
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.json({ received: true });
});
export default app.init();
Deploy:
coho deploy
Project: mystripehandler-a1b2 Space: dev
Deployed Codehook successfully! 🙌
Run coho info to see your API endpoints and tokens. Your webhook URL will be:
https://mystripehandler-a1b2.api.codehooks.io/dev/stripe/webhooks
Configure Stripe Dashboard
- Go to Stripe Dashboard → Webhooks
- Click Add endpoint
- Enter your Codehooks URL:
https://your-app.api.codehooks.io/dev/stripe/webhooks - Select events to listen for (or choose "All events")
- Copy the Signing secret and add it to your Codehooks secrets:
coho set-env STRIPE_WEBHOOK_SECRET whsec_xxxxx --encrypted
coho set-env STRIPE_SECRET_KEY sk_xxxxx --encrypted
Finding Your STRIPE_WEBHOOK_SECRET
Your Stripe webhook signing secret (starting with whsec_) is required for signature verification. Here's how to find it:
- Go to Stripe Dashboard → Developers → Webhooks
- Click on your webhook endpoint (or create one if you haven't already)
- In the endpoint details, find Signing secret
- Click Reveal to show the secret (it starts with
whsec_) - Copy the secret and add it to your Codehooks environment:
coho set-env STRIPE_WEBHOOK_SECRET whsec_your_secret_here --encrypted
Never commit your STRIPE_WEBHOOK_SECRET to version control. The --encrypted flag ensures it's stored securely in Codehooks. Each webhook endpoint has its own unique signing secret—test and live mode endpoints have different secrets.
invoice.paid vs checkout.session.completed
One of the most common questions when building Stripe integrations is which event to listen for: invoice.paid or checkout.session.completed? Here's when to use each:
checkout.session.completed
Use this event for one-time payments and initial Checkout sessions:
- Triggers when a customer completes Stripe Checkout
- Best for one-time purchases, donations, or initial subscription signups
- Contains the full Checkout Session object with customer and payment details
case 'checkout.session.completed':
const session = event.data.object;
if (session.mode === 'payment') {
// One-time payment - fulfill the order
await fulfillOrder(session);
} else if (session.mode === 'subscription') {
// Initial subscription - provision access
await provisionSubscription(session);
}
break;
invoice.paid
Use this event for subscriptions and recurring payments:
- Triggers every time an invoice is successfully paid
- Covers both initial subscription payments AND renewals
- Essential for SaaS apps with recurring billing
case 'invoice.paid':
const invoice = event.data.object;
// Extend or renew subscription access
await extendSubscriptionAccess(invoice.subscription, invoice.customer);
break;
Recommendation for SaaS Apps
For most SaaS applications, listen to invoice.paid as your primary event. It fires for:
- Initial subscription payments
- Monthly/yearly renewals
- Plan upgrades that generate prorated invoices
- Manual invoice payments
If you need to capture the exact moment a customer completes checkout (for analytics, welcome emails, etc.), also listen to checkout.session.completed.
Typical SaaS setup:
checkout.session.completed→ Send welcome email, track conversioninvoice.paid→ Extend subscription access, update billing recordscustomer.subscription.deleted→ Revoke access
Common Stripe Webhook Events
| Event | Description | Common Use Case |
|---|---|---|
payment_intent.succeeded | Payment completed successfully | Update order status, send receipt |
payment_intent.payment_failed | Payment attempt failed | Notify customer, retry logic |
invoice.paid | Invoice payment succeeded | Extend subscription access |
invoice.payment_failed | Invoice payment failed | Send dunning email |
customer.subscription.created | New subscription started | Provision access |
customer.subscription.updated | Subscription changed | Update plan limits |
customer.subscription.deleted | Subscription canceled | Revoke access |
checkout.session.completed | Checkout completed | Fulfill order |
charge.dispute.created | Dispute opened | Alert team, gather evidence |
Event Payload Types: Snapshot vs Thin Events
Stripe offers two event payload formats:
- Snapshot events (default): Include a full copy of the affected object at the time of the event. This is the traditional format most integrations use.
- Thin events: Include only the event type and object ID. Your handler must fetch the current object state via the Stripe API.
For most use cases, snapshot events are recommended as they provide all the data you need without additional API calls. Use thin events when you always need the latest state or want to reduce payload size.
Stripe Delivery and Retry Behavior
Stripe attempts to deliver webhooks for up to 3 days with exponential backoff:
- If your endpoint returns a non-2xx response, Stripe retries
- Events may arrive out of order (use
createdtimestamp for ordering) - The same event may be delivered multiple times (use
event.idfor idempotency)
Stripe Webhook Signature Verification
Stripe signs all webhook payloads with a signature header (Stripe-Signature). Always verify this signature to ensure the webhook came from Stripe:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
function verifyStripeWebhook(req) {
const sig = req.headers['stripe-signature'];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
try {
return stripe.webhooks.constructEvent(
req.rawBody, // Raw request body (not parsed JSON)
sig,
endpointSecret
);
} catch (err) {
throw new Error(`Webhook Error: ${err.message}`);
}
}
Important: Use req.rawBody (the raw string), not req.body (parsed JSON). Signature verification requires the exact bytes Stripe sent.
Monitoring Stripe Webhooks
Effective monitoring helps you catch issues before they impact customers. Here's how to monitor your Stripe webhooks:
Stripe Dashboard Monitoring
The Stripe Dashboard provides built-in webhook monitoring:
- Go to Stripe Dashboard → Developers → Webhooks
- Click on your endpoint to see:
- Delivery success rate — percentage of successfully delivered webhooks
- Recent deliveries — list of recent webhook attempts with status codes
- Failed events — webhooks that failed and their retry status
Failed webhooks show the HTTP status code and response body, making debugging straightforward.
Real-Time Logs with Codehooks
Codehooks automatically logs all incoming webhooks. Monitor them in real-time:
coho logs --follow
This streams all webhook activity, including:
- Incoming requests with timestamps
- Your application's console.log output
- Errors and stack traces
For a specific number of recent log entries:
coho logs --tail 50
What to Monitor
Key metrics to watch:
- Signature verification failures — may indicate misconfigured secrets or attack attempts
- Processing errors — exceptions in your event handling code
- Response times — ensure you return 200 within 20 seconds
- Event types — unexpected events may indicate Stripe configuration changes
Best Practices for Stripe Webhooks
1. Return 200 Quickly
Stripe expects a 2xx response within 20 seconds. Store the event first, then return immediately. Process complex logic later:
import { app, Datastore } from 'codehooks-js';
app.post('/stripe/webhooks', async (req, res) => {
// Verify signature first
const event = verifyStripeWebhook(req);
const conn = await Datastore.open();
// Check if we already have this event (idempotency)
try {
await conn.getOne('stripe_events', event.id);
return res.json({ received: true }); // Already exists
} catch (e) {
// Not found, continue to store
}
// Store new event
await conn.insertOne('stripe_events', {
_id: event.id,
type: event.type,
rawBody: req.rawBody, // Store raw body for replay capability
created: event.created,
processedAt: null
});
res.json({ received: true });
});
2. Handle Duplicate Events
Stripe may send the same event multiple times. Use idempotency:
import { Datastore } from 'codehooks-js';
async function handleStripeEvent(event) {
const conn = await Datastore.open();
// Check if we've already processed this event
try {
await conn.getOne('processed_events', event.id);
console.log('Event already processed:', event.id);
return;
} catch (e) {
// Not found, continue to process
}
// Process the event
await processEvent(event);
// Mark as processed
await conn.insertOne('processed_events', {
_id: event.id,
type: event.type,
processedAt: new Date().toISOString()
});
}
3. Handle Event Ordering
Events may arrive out of order. Check timestamps or use created field:
case 'customer.subscription.updated':
const conn = await Datastore.open();
const subscription = event.data.object;
let existingRecord = null;
try {
existingRecord = await conn.getOne('subscriptions', subscription.id);
} catch (e) {
// Not found, will create new record
}
// Only update if this event is newer
if (!existingRecord || event.created > existingRecord.lastEventTime) {
await conn.updateOne('subscriptions', subscription.id, {
$set: {
status: subscription.status,
lastEventTime: event.created
}
});
}
break;
Why Use Codehooks.io for Stripe Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---|---|---|
| Setup time | Hours/Days | 5 minutes |
| Signature verification | Manual implementation | Built-in support |
| Scaling | Configure infrastructure | Automatic |
| Retries | Build retry logic | Platform handles |
| Monitoring | Set up logging | Built-in logs |
| Cost | Server costs + maintenance | Free tier available |
Related Resources
- Stripe Webhook Template - Ready-to-deploy template
- All Codehooks Templates - Browse all webhook templates
- Stripe Webhooks Documentation
- Codehooks.io Quick Start
- Secure Zapier/Make/n8n/IFTTT Webhooks - Forward verified Stripe events to automation platforms
- Webhook Delivery System - Build your own outgoing webhook system
- What are Webhooks? - Learn webhook fundamentals
- PayPal Webhooks Example - Alternative payment provider
- Shopify Webhooks Example - E-commerce webhook handler
- Browse All Webhook Examples - Explore all webhook integration examples
- Quick Deploy Templates - Ready-to-deploy backend templates
Stripe Webhooks Integration FAQ
Common questions about Stripe webhook integration
How do I build a Stripe webhooks integration?
How do I verify Stripe webhook signatures?
stripe.webhooks.constructEvent() method with your webhook signing secret. This verifies the Stripe-Signature header matches the payload. Always use the raw request body (not parsed JSON) for verification. The signing secret is found in your Stripe Dashboard under Webhooks → your endpoint → Signing secret.Why am I getting 'No signatures found matching the expected signature' error?
req.rawBody not req.body, or (3) The webhook was sent to a different endpoint than configured. Double-check your STRIPE_WEBHOOK_SECRET environment variable.How do I test my Stripe webhooks integration?
coho deploy and you have a live public URL in seconds. Configure that URL in the Stripe Dashboard, then use Stripe's 'Send test webhook' button or trigger real test events in test mode. Check logs with coho logs.What happens if my webhook endpoint is down?
Should I handle all Stripe webhook events?
payment_intent.succeeded, invoice.paid, customer.subscription.created/updated/deleted, and checkout.session.completed. Handling unnecessary events wastes resources and adds complexity.How do I handle duplicate Stripe webhook events?
event.id exists in your database before processing, and save it after successful handling. This prevents duplicate order fulfillment or email sends.What's the difference between test and live webhook endpoints?
How quickly must I respond to Stripe webhooks?
What's the difference between invoice.paid and checkout.session.completed?
checkout.session.completed for one-time payments and capturing the initial checkout moment. Use invoice.paid for subscriptions and recurring payments—it fires for both initial and renewal payments. For SaaS apps, listen to invoice.paid as your primary event since it covers all subscription billing scenarios. See the invoice.paid vs checkout.session.completed section above for detailed guidance.How do I find my Stripe webhook signing secret?
whsec_. Find it in Stripe Dashboard → Developers → Webhooks → click your endpoint → Signing secret → Reveal. Add it to Codehooks with: coho set-env STRIPE_WEBHOOK_SECRET whsec_xxx --encrypted. See Finding Your STRIPE_WEBHOOK_SECRET for step-by-step instructions.