Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to listen to subscription events in Stripe

Stripe sends subscription lifecycle events via webhooks. Listen for customer.subscription.created, updated, deleted, and invoice.payment_failed to keep your app in sync. Set up a webhook endpoint, verify signatures with stripe.webhooks.constructEvent, and handle each event type to manage access, send emails, and update your database.

What you'll learn

  • Which Stripe subscription webhook events matter most
  • How to set up a webhook endpoint with signature verification
  • How to handle subscription created, updated, and canceled events
  • How to respond to failed invoice payments in real time
  • How to test subscription webhooks with the Stripe CLI
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read25 minutesStripe API v2024-12+, Node.js 18+, Express 4+March 2026RapidDev Engineering Team
TL;DR

Stripe sends subscription lifecycle events via webhooks. Listen for customer.subscription.created, updated, deleted, and invoice.payment_failed to keep your app in sync. Set up a webhook endpoint, verify signatures with stripe.webhooks.constructEvent, and handle each event type to manage access, send emails, and update your database.

Why Subscription Webhooks Are Essential

Stripe uses webhooks to notify your server about subscription changes in real time. Without webhooks, your app has no reliable way to know when a customer upgrades, downgrades, cancels, or fails a payment. Polling the API is slow and wasteful. Webhooks give you instant, push-based notifications so you can update user access, trigger emails, and keep your billing database accurate.

Prerequisites

  • A Stripe account in test mode with a product and price created
  • Node.js 18+ and npm installed locally
  • Basic familiarity with Express.js or a similar Node.js framework
  • Stripe CLI installed for local webhook testing
  • A database or user management system to update subscription status

Step-by-step guide

1

Install Stripe SDK and set up Express

Initialize your project and install the Stripe Node.js library along with Express. Store your Stripe secret key and webhook signing secret as environment variables — never hard-code them.

typescript
1npm init -y
2npm install stripe express dotenv

Expected result: Project initialized with stripe, express, and dotenv in package.json dependencies.

2

Create the webhook endpoint with raw body parsing

Stripe requires the raw request body to verify webhook signatures. Use express.raw() for the webhook route instead of express.json(). This is the most common source of signature verification failures.

typescript
1const express = require('express');
2const app = express();
3
4// IMPORTANT: webhook route must use raw body
5app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
6 // Webhook handling goes here
7 res.sendStatus(200);
8});
9
10// Other routes use JSON parsing
11app.use(express.json());
12
13app.listen(3000, () => console.log('Server running on port 3000'));

Expected result: Express server running on port 3000 with a /webhook route accepting raw body POST requests.

3

Verify the webhook signature

Use stripe.webhooks.constructEvent to verify that the event genuinely came from Stripe. This prevents attackers from sending fake events to your endpoint.

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
3
4app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
5 const sig = req.headers['stripe-signature'];
6 let event;
7
8 try {
9 event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
10 } catch (err) {
11 console.error('Webhook signature verification failed:', err.message);
12 return res.status(400).send(`Webhook Error: ${err.message}`);
13 }
14
15 // Handle the event
16 res.json({ received: true });
17});

Expected result: Webhook endpoint verifies each incoming event signature. Invalid signatures return 400; valid events return 200.

4

Handle key subscription events

Route each event type to the appropriate handler. The most important subscription events are: customer.subscription.created (new sub), customer.subscription.updated (plan change, renewal), customer.subscription.deleted (cancellation), and invoice.payment_failed (payment issue).

typescript
1switch (event.type) {
2 case 'customer.subscription.created':
3 const newSub = event.data.object;
4 await activateSubscription(newSub.customer, newSub.id, newSub.items.data[0].price.id);
5 break;
6
7 case 'customer.subscription.updated':
8 const updatedSub = event.data.object;
9 await updateSubscription(updatedSub.customer, updatedSub.status, updatedSub.items.data[0].price.id);
10 break;
11
12 case 'customer.subscription.deleted':
13 const canceledSub = event.data.object;
14 await deactivateSubscription(canceledSub.customer);
15 break;
16
17 case 'invoice.payment_failed':
18 const failedInvoice = event.data.object;
19 await handleFailedPayment(failedInvoice.customer, failedInvoice.id);
20 break;
21
22 default:
23 console.log(`Unhandled event type: ${event.type}`);
24}

Expected result: Each subscription lifecycle event triggers the corresponding business logic in your application.

5

Test with the Stripe CLI

Use the Stripe CLI to forward test webhook events to your local server. This lets you simulate the full subscription lifecycle without creating real subscriptions.

typescript
1# In terminal 1 forward events to your local server
2stripe listen --forward-to localhost:3000/webhook
3
4# In terminal 2 trigger a test event
5stripe trigger customer.subscription.created

Expected result: Stripe CLI forwards the test event to your local server. Your console logs confirm the event was received and processed.

6

Register the webhook in the Stripe Dashboard for production

Go to Stripe Dashboard → Developers → Webhooks → Add endpoint. Enter your production URL (e.g., https://yourapp.com/webhook). Select the events: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed. Copy the signing secret and add it to your production environment variables.

Expected result: Webhook endpoint registered in Stripe Dashboard. Stripe will send subscription events to your production URL.

Complete working example

server.js
1require('dotenv').config();
2const express = require('express');
3const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
4
5const app = express();
6const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
7
8// Webhook route — must use raw body for signature verification
9app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
10 const sig = req.headers['stripe-signature'];
11 let event;
12
13 try {
14 event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
15 } catch (err) {
16 console.error('Webhook signature verification failed:', err.message);
17 return res.status(400).send(`Webhook Error: ${err.message}`);
18 }
19
20 try {
21 switch (event.type) {
22 case 'customer.subscription.created': {
23 const sub = event.data.object;
24 console.log(`New subscription ${sub.id} for customer ${sub.customer}`);
25 // TODO: activate user access in your database
26 break;
27 }
28 case 'customer.subscription.updated': {
29 const sub = event.data.object;
30 console.log(`Subscription ${sub.id} updated — status: ${sub.status}`);
31 // TODO: update plan tier, handle trial end, etc.
32 break;
33 }
34 case 'customer.subscription.deleted': {
35 const sub = event.data.object;
36 console.log(`Subscription ${sub.id} canceled for customer ${sub.customer}`);
37 // TODO: revoke user access
38 break;
39 }
40 case 'invoice.payment_failed': {
41 const invoice = event.data.object;
42 console.log(`Payment failed for invoice ${invoice.id}, customer ${invoice.customer}`);
43 // TODO: notify user, flag account
44 break;
45 }
46 default:
47 console.log(`Unhandled event type: ${event.type}`);
48 }
49 } catch (err) {
50 console.error('Error processing webhook:', err);
51 return res.status(500).json({ error: 'Webhook handler failed' });
52 }
53
54 res.json({ received: true });
55});
56
57// Other routes use JSON
58app.use(express.json());
59
60app.get('/', (req, res) => {
61 res.send('Stripe Subscription Webhook Server');
62});
63
64const PORT = process.env.PORT || 3000;
65app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Common mistakes when listening to subscription events in Stripe

Why it's a problem: Using express.json() for the webhook route instead of express.raw()

How to avoid: Stripe signature verification requires the raw request body. Use express.raw({ type: 'application/json' }) specifically for your webhook endpoint.

Why it's a problem: Not returning a 200 response quickly enough

How to avoid: Stripe expects a 2xx response within 20 seconds. Process heavy logic asynchronously (e.g., queue the event) and return 200 immediately.

Why it's a problem: Using the test mode webhook secret in production

How to avoid: Test and live mode have separate webhook signing secrets. Update your STRIPE_WEBHOOK_SECRET environment variable when deploying to production.

Why it's a problem: Not handling duplicate events

How to avoid: Stripe may send the same event more than once. Store processed event IDs and check for duplicates before processing.

Best practices

  • Always verify webhook signatures using stripe.webhooks.constructEvent — never trust raw POST data
  • Return a 200 status code immediately and process events asynchronously for heavy operations
  • Store the event ID and check for duplicates to make your webhook handler idempotent
  • Log all received events with their type and ID for debugging
  • Use the Stripe CLI to test webhooks locally before deploying
  • Subscribe only to the events you need — avoid catching all events unnecessarily
  • Set up alerts for repeated webhook failures in the Stripe Dashboard
  • Keep your webhook signing secret in environment variables, never in source code

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Write a Node.js Express webhook endpoint that listens for Stripe subscription events (created, updated, deleted, invoice.payment_failed). Verify the webhook signature using stripe.webhooks.constructEvent with raw body parsing. Include error handling and a switch statement for each event type.

Stripe Prompt

Build a Stripe webhook handler in Node.js that processes subscription lifecycle events. Use express.raw() for the webhook route, verify signatures with constructEvent, and handle customer.subscription.created, updated, deleted, and invoice.payment_failed events.

Frequently asked questions

Which Stripe subscription events should I listen to?

At minimum, listen to customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, and invoice.payment_failed. These cover the full subscription lifecycle: activation, plan changes, cancellation, and payment failures.

How do I test subscription webhooks locally?

Install the Stripe CLI and run 'stripe listen --forward-to localhost:3000/webhook'. This forwards events to your local server. Use 'stripe trigger customer.subscription.created' to simulate events.

Why is my webhook returning a 400 error?

The most common cause is parsing the request body as JSON before signature verification. Your webhook route must use express.raw({ type: 'application/json' }) instead of express.json() to preserve the raw body for signature checking.

Can Stripe send the same webhook event twice?

Yes. Stripe retries failed webhook deliveries up to 3 days. Always store processed event IDs and check for duplicates to make your handler idempotent.

What happens if my webhook endpoint is down?

Stripe retries webhook deliveries with exponential backoff over 72 hours. After repeated failures, Stripe disables the endpoint and sends you an email notification. You can manually retry missed events from the Dashboard.

Should I process webhook events synchronously or asynchronously?

Return a 200 response immediately and process the event asynchronously. Stripe times out webhook requests after 20 seconds. Use a message queue or background job for heavy processing like database updates or email sends.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.