Skip to main content

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.

Stripe webhooks integration with Codehooks.io

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.

Use the Template

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! 🙌
Finding your API endpoint

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

  1. Go to Stripe Dashboard → Webhooks
  2. Click Add endpoint
  3. Enter your Codehooks URL: https://your-app.api.codehooks.io/dev/stripe/webhooks
  4. Select events to listen for (or choose "All events")
  5. 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

EventDescriptionCommon Use Case
payment_intent.succeededPayment completed successfullyUpdate order status, send receipt
payment_intent.payment_failedPayment attempt failedNotify customer, retry logic
invoice.paidInvoice payment succeededExtend subscription access
invoice.payment_failedInvoice payment failedSend dunning email
customer.subscription.createdNew subscription startedProvision access
customer.subscription.updatedSubscription changedUpdate plan limits
customer.subscription.deletedSubscription canceledRevoke access
checkout.session.completedCheckout completedFulfill order
charge.dispute.createdDispute openedAlert 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 created timestamp for ordering)
  • The same event may be delivered multiple times (use event.id for 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?

FeatureDIY SetupCodehooks.io
Setup timeHours/Days5 minutes
Signature verificationManual implementationBuilt-in support
ScalingConfigure infrastructureAutomatic
RetriesBuild retry logicPlatform handles
MonitoringSet up loggingBuilt-in logs
CostServer costs + maintenanceFree tier available

Stripe Webhooks FAQ

Common questions about handling Stripe webhooks

How do I verify Stripe webhook signatures?
Use the 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?
This usually happens when: (1) You're using the wrong webhook secret (test vs live mode), (2) The request body was parsed before verification - use 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?
With Codehooks.io, you don't need local testing or tunneling tools. Just run 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?
Stripe retries failed webhook deliveries for up to 3 days using exponential backoff. The retry schedule is: immediately, 5 min, 30 min, 2 hours, 5 hours, 10 hours, then every 12 hours. You can also manually retry from the Stripe Dashboard under Webhooks → Failed events.
Should I handle all Stripe webhook events?
No, only subscribe to events your application needs. Common essential events include 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?
Stripe may send the same event multiple times. Implement idempotency by storing processed event IDs: check if 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?
You need separate webhook endpoints for test mode (using test API keys) and live mode (using live API keys). Each has its own signing secret. Configure both in the Stripe Dashboard. Use test mode for development and staging environments.
How quickly must I respond to Stripe webhooks?
Return a 2xx response within 20 seconds, or Stripe considers the delivery failed and will retry. For complex processing, return 200 immediately after signature verification, then process the event asynchronously using a queue or background job.