/stripe-guides

How to listen to subscription events in Stripe?

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.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to listen to subscription events in Stripe?

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

 

  1. Go to the Stripe Dashboard
  2. Navigate to Developers > Webhooks
  3. Click "Add endpoint"
  4. Enter the URL of your webhook endpoint (e.g., https://your-domain.com/webhook)
  5. Select the events you want to listen for (for subscriptions, look for events starting with "customer.subscription")

Common subscription events include:

  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • invoice.payment\_succeeded
  • invoice.payment\_failed

 

Step 5: Test Your Webhook with the CLI (Optional but Recommended)

 

Stripe provides a CLI tool that makes testing webhooks easier during development:

  1. Install the Stripe CLI from https://stripe.com/docs/stripe-cli
  2. Login to your Stripe account:
stripe login
  1. Forward events to your local server:
stripe listen --forward-to http://localhost:3000/webhook
  1. The CLI will output a webhook signing secret. Use this secret in your webhook handler code.

 

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:

  • id: The unique identifier for the subscription
  • customer: The ID of the customer who owns the subscription
  • status: The status of the subscription (active, past\_due, canceled, etc.)
  • current_period_start and current_period_end: The billing cycle dates
  • items: The products/prices included in the subscription
  • cancel_at_period\_end: Whether the subscription will be canceled at the end of the current period

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:

  1. Update your Stripe API keys to use your live keys
  2. Set up a production webhook endpoint in the Stripe Dashboard
  3. Ensure your webhook endpoint is available via HTTPS
  4. Update the webhook signing secret in your code

 

Step 10: Monitor and Debug

 

Stripe provides tools to monitor and debug webhooks:

  1. In the Stripe Dashboard, go to Developers > Webhooks
  2. Click on an endpoint to see the recent events
  3. For each event, you can see:
    • The full request and response
    • Whether the delivery was successful
    • The timing of the request

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:

  1. Go to Developers > Webhooks
  2. Click on your webhook endpoint
  3. Click "Edit" and then scroll down to "Notification settings"
  4. Enter your email to receive notifications about failed webhook deliveries

 

Step 14: Testing Different Subscription Scenarios

 

Use the Stripe CLI to test different subscription scenarios:

  1. Create a new subscription:
stripe trigger customer.subscription.created
  1. Update a subscription:
stripe trigger customer.subscription.updated
  1. Cancel a subscription:
stripe trigger customer.subscription.deleted
  1. Test a successful payment:
stripe trigger invoice.payment\_succeeded
  1. Test a failed payment:
stripe trigger invoice.payment\_failed

 

Step 15: Implement a Subscription Webhook Checklist

 

For a robust subscription system, ensure you're handling these key scenarios:

  • New subscriptions (customer.subscription.created)
  • Subscription renewals (invoice.payment\_succeeded with existing subscription)
  • Failed payments (invoice.payment\_failed)
  • Plan changes (customer.subscription.updated with different price/product)
  • Cancellations (customer.subscription.deleted or updated with cancel_at_period\_end=true)
  • Past due subscriptions (customer.subscription.updated with status=past\_due)
  • Subscription reactivations (customer.subscription.updated from non-active to active)

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.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022