Learn how to set up and handle Stripe subscription events using webhooks. Step-by-step guide for creating endpoints, processing events, and testing with the Stripe CLI.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
How to Listen to Subscription Events in Stripe
In this comprehensive tutorial, I'll guide you through the process of setting up and listening to subscription events in Stripe. Stripe uses webhooks to notify your application about events that happen in your account, such as when a subscription is created, updated, or canceled.
Step 1: Create a Stripe Account
First, you need to have a Stripe account. If you don't have one, go to https://stripe.com and sign up. Once you have an account, you'll have access to both test and live modes. For development purposes, always use the test mode.
Step 2: Install the Stripe SDK
Depending on your programming language, you'll need to install the Stripe SDK. Here are examples for some common languages:
For Node.js:
npm install stripe
For Python:
pip install stripe
For PHP:
composer require stripe/stripe-php
For Ruby:
gem install stripe
Step 3: Set Up Your Webhook Endpoint
Create an endpoint in your application that will receive webhook events from Stripe. Here's an example using Node.js and Express:
const express = require('express');
const stripe = require('stripe')('sk_test_your_secret_key');
const bodyParser = require('body-parser');
const app = express();
// Use JSON parser for webhooks
app.post('/webhook', bodyParser.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
'whsec_your_webhook_signing_secret'
);
} catch (err) {
console.log(`Webhook Error: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
console.log('Received event:', event.type);
// Return a response to acknowledge receipt of the event
res.json({received: true});
});
app.listen(3000, () => console.log('Running on port 3000'));
Step 4: Register Your Webhook in the Stripe Dashboard
Common subscription events include:
Step 5: Test Your Webhook with the CLI (Optional but Recommended)
Stripe provides a CLI tool that makes testing webhooks easier during development:
stripe login
stripe listen --forward-to http://localhost:3000/webhook
Step 6: Handle Subscription Events
Now, let's modify our webhook handler to specifically handle subscription events:
app.post('/webhook', bodyParser.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
'whsec_your_webhook_signing_secret'
);
} catch (err) {
console.log(`Webhook Error: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle specific subscription events
switch (event.type) {
case 'customer.subscription.created':
const subscriptionCreated = event.data.object;
console.log(`Subscription created: ${subscriptionCreated.id}`);
// Add your business logic for new subscriptions
await handleNewSubscription(subscriptionCreated);
break;
case 'customer.subscription.updated':
const subscriptionUpdated = event.data.object;
console.log(`Subscription updated: ${subscriptionUpdated.id}`);
// Add your business logic for subscription updates
await handleUpdatedSubscription(subscriptionUpdated);
break;
case 'customer.subscription.deleted':
const subscriptionDeleted = event.data.object;
console.log(`Subscription canceled: ${subscriptionDeleted.id}`);
// Add your business logic for canceled subscriptions
await handleCanceledSubscription(subscriptionDeleted);
break;
case 'invoice.payment\_succeeded':
const invoice = event.data.object;
// Only handle subscription invoices
if (invoice.subscription) {
console.log(`Payment succeeded for subscription: ${invoice.subscription}`);
await handleSuccessfulPayment(invoice);
}
break;
case 'invoice.payment\_failed':
const failedInvoice = event.data.object;
// Only handle subscription invoices
if (failedInvoice.subscription) {
console.log(`Payment failed for subscription: ${failedInvoice.subscription}`);
await handleFailedPayment(failedInvoice);
}
break;
default:
// Unexpected event type
console.log(`Unhandled event type: ${event.type}`);
}
// Return a response to acknowledge receipt of the event
res.json({received: true});
});
// Implement these functions according to your business logic
async function handleNewSubscription(subscription) {
// E.g., activate a user's premium features
// Update your database
}
async function handleUpdatedSubscription(subscription) {
// E.g., change user's plan or features
// Update your database
}
async function handleCanceledSubscription(subscription) {
// E.g., deactivate premium features
// Update your database
}
async function handleSuccessfulPayment(invoice) {
// E.g., extend subscription period
// Update your database
}
async function handleFailedPayment(invoice) {
// E.g., notify user of payment failure
// Update your database
}
Step 7: Understand the Subscription Object
The subscription object contains important information that you'll want to store in your database:
Here's an example of accessing these properties:
async function handleNewSubscription(subscription) {
// Extract important details
const customerId = subscription.customer;
const subscriptionId = subscription.id;
const status = subscription.status;
const currentPeriodEnd = new Date(subscription.current_period_end \* 1000);
// Get the plan/price information
const priceId = subscription.items.data[0].price.id;
const productId = subscription.items.data[0].price.product;
// Update your database with this information
await database.updateUserSubscription({
userId: findUserByCustomerId(customerId),
subscriptionId: subscriptionId,
status: status,
priceId: priceId,
productId: productId,
expiresAt: currentPeriodEnd
});
console.log(`Updated subscription for customer ${customerId}`);
}
Step 8: Implement Idempotency
Stripe may send the same webhook multiple times to ensure delivery. To avoid processing the same event twice, implement idempotency:
app.post('/webhook', bodyParser.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
'whsec_your_webhook_signing_secret'
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Check if we've processed this event before
const eventId = event.id;
const existingEvent = await database.findProcessedEvent(eventId);
if (existingEvent) {
// We've already processed this event
console.log(`Event ${eventId} already processed`);
return res.json({received: true});
}
// Mark this event as processing
await database.saveProcessedEvent(eventId);
// Process the event (similar to Step 6)
switch (event.type) {
case 'customer.subscription.created':
// Handle event
break;
// other cases...
}
// Return a response
res.json({received: true});
});
Step 9: Deploy to Production
When you're ready to go live:
Step 10: Monitor and Debug
Stripe provides tools to monitor and debug webhooks:
You can also resend webhook events from the dashboard for testing purposes.
Step 11: Error Handling and Retries
Implement proper error handling in your webhook processing code:
app.post('/webhook', bodyParser.raw({type: 'application/json'}), async (req, res) => {
// Verify and extract the event as before...
try {
// Process the event
switch (event.type) {
case 'customer.subscription.created':
await handleNewSubscription(event.data.object);
break;
// Other cases...
}
// Mark the event as successfully processed
await database.markEventSuccess(event.id);
// Return success response
res.json({received: true});
} catch (error) {
console.error(`Error processing webhook: ${error}`);
// Mark the event as failed but don't return an error to Stripe
// so Stripe won't retry immediately (you'll handle retries yourself)
await database.markEventFailed(event.id, error.message);
// Still return a 200 status to acknowledge receipt
res.json({received: true, error: error.message});
// Optionally, queue this event for retry later
await queueForRetry(event);
}
});
Step 12: Advanced: Using Event Types for Subscription Status
Instead of just relying on the subscription status, you can use the event types to determine what happened:
async function processSubscriptionEvent(event) {
const subscription = event.data.object;
const customerId = subscription.customer;
const userId = await getUserIdFromCustomerId(customerId);
switch (event.type) {
case 'customer.subscription.created':
await activateSubscription(userId, subscription);
break;
case 'customer.subscription.updated':
// Check what changed
if (subscription.cancel_at_period\_end) {
await markSubscriptionForCancellation(userId, subscription);
} else if (subscription.status === 'past\_due') {
await handlePastDueSubscription(userId, subscription);
} else if (subscription.status === 'active' && subscription.items.data[0].price.id !== getCurrentPriceId(userId)) {
await handlePlanChange(userId, subscription);
}
break;
case 'customer.subscription.deleted':
await deactivateSubscription(userId, subscription);
break;
}
}
Step 13: Set Up Email Notifications for Failed Webhooks
In the Stripe Dashboard:
Step 14: Testing Different Subscription Scenarios
Use the Stripe CLI to test different subscription scenarios:
stripe trigger customer.subscription.created
stripe trigger customer.subscription.updated
stripe trigger customer.subscription.deleted
stripe trigger invoice.payment\_succeeded
stripe trigger invoice.payment\_failed
Step 15: Implement a Subscription Webhook Checklist
For a robust subscription system, ensure you're handling these key scenarios:
For each of these scenarios, implement the appropriate business logic in your application.
By following these steps, you'll have a robust system for listening to and handling Stripe subscription events. This will enable you to maintain accurate subscription states in your application and provide a seamless experience for your users.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.