How to Receive Stripe Webhooks with Examples
Stripe webhooks (also called event destinations) notify your application when events happen in your 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.
The solution? Deploy a production-ready Stripe webhook handler with Codehooks.io in under 5 minutes using our ready-made template.
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
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.
Best Practices for Stripe Webhooks
1. Return 200 Quickly
Stripe expects a 2xx response within 20 seconds. Process complex logic asynchronously:
app.post('/stripe/webhooks', async (req, res) => {
// Verify signature first
const event = verifyStripeWebhook(req);
// Return 200 immediately
res.json({ received: true });
// Process asynchronously (Codehooks queues are perfect for this)
await processStripeEventAsync(event);
});
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
const existing = await conn.getOne('processed_events', event.id);
if (existing) {
console.log('Event already processed:', event.id);
return;
}
// 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;
const existingRecord = await conn.getOne('subscriptions', subscription.id);
// 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
- Webhook Delivery System - Build your own outgoing webhook system
Stripe Webhooks FAQ
Common questions about handling Stripe webhooks
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 Stripe webhooks?
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.