Felix Khafizov

EN

Changing Stripe statement descriptors for subscription payments

The Challenge

A client requires that each subscription payment appear with a unique statement descriptor on their bank statements, even though the underlying product and price remain the same for every billing cycle.

The Solution

For one-time charges, dynamically altering the bank statement information is straightforward using the statement_descriptor_suffix parameter. However, this field is not available on a Subscription object to modify future payments.

The correct approach for subscriptions is to update each individual invoice as it’s generated. Stripe creates an invoice approximately one hour before attempting to charge the customer. By intercepting this invoice, we can set a specific statement descriptor for that particular payment.

This can be achieved by creating a webhook handler that listens for the invoice.created event. When your application receives this event, it should make an API call to update the newly created invoice, setting its statement_descriptor property.

Here is a sample implementation in an Express-like framework:

// Import necessary libraries
import express from 'express'
import Stripe from 'stripe'
import crypto from 'crypto'
 
// --- Configuration ---
// It's recommended to store these in environment variables for security
const STRIPE_SECRET_KEY =
  process.env.STRIPE_SECRET_KEY || 'your_stripe_secret_key'
const STRIPE_WEBHOOK_SECRET =
  process.env.STRIPE_WEBHOOK_SECRET || 'your_stripe_webhook_secret'
const STRIPE_SUBSCRIPTION_PRICE_ID =
  process.env.STRIPE_SUBSCRIPTION_PRICE_ID || 'your_price_id'
 
// Initialize Stripe with the secret key
const stripe = new Stripe(STRIPE_SECRET_KEY)
 
// Initialize the Express app
const app = express()
 
// --- Webhook Endpoint ---
// Use express.raw() to get the raw request body, which is required for Stripe's signature verification.
// This middleware must come before any other body-parsing middleware like express.json().
app.post(
  '/webhook',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    // Check if the webhook secret is configured
    if (!STRIPE_WEBHOOK_SECRET) {
      console.error(
        'Stripe webhook secret is not set. Please set the STRIPE_WEBHOOK_SECRET environment variable.'
      )
      return res.status(500).json({ error: 'Webhook secret not configured.' })
    }
 
    // Get the signature from the request headers
    const signature = req.headers['stripe-signature']
 
    let event
 
    try {
      // Verify the event came from Stripe using the raw body and signature
      event = stripe.webhooks.constructEvent(
        req.body,
        signature,
        STRIPE_WEBHOOK_SECRET
      )
    } catch (err) {
      console.error(`Webhook signature verification failed: ${err.message}`)
      return res.status(400).send(`Webhook Error: ${err.message}`)
    }
 
    // --- Handle the Event ---
    try {
      // Handle the specific 'invoice.created' event
      if (event.type === 'invoice.created') {
        const invoice = event.data.object
 
        // Extract necessary data from the invoice
        const customerId = invoice.customer
        const priceId = invoice.lines.data[0]?.price?.id
 
        // Check conditions: invoice is a draft, has a customer, and matches the specific subscription price ID
        if (
          invoice.status === 'draft' &&
          customerId &&
          priceId === STRIPE_SUBSCRIPTION_PRICE_ID
        ) {
          // Prepare the new statement descriptor
          const currentDescriptor = invoice.statement_descriptor // The one that is set as Public Descriptor in Stripe Dashboard
          const suffix = 'some_suffix' // Your additional suffix
 
          // Combine descriptor and formatted ID, ensuring it fits within Stripe's 22-character limit
          const newDescriptor = `${currentDescriptor} ${suffix}`.slice(0, 22) // 22 is the limit for the statement descriptor
 
          // Update the invoice on Stripe's end
          await stripe.invoices.update(invoice.id, {
            statement_descriptor: newDescriptor,
          })
 
          console.log(
            `Updated invoice ${invoice.id} statement descriptor to "${newDescriptor}" for customerId: ${customerId}`
          )
        }
      }
    } catch (error) {
      // Catch any errors during event processing
      console.error('Error processing stripe webhook event:', error)
      // Return a 500 error but don't prevent Stripe from thinking the webhook was received.
      // Stripe will retry if it doesn't get a 2xx response, so sending a success response
      // in the `finally` block is important to prevent duplicate event handling.
    } finally {
      // Acknowledge receipt of the event to Stripe
      res.status(200).json({ message: 'Webhook received successfully' })
    }
  }
)
 
// --- Server Startup ---
const PORT = process.env.PORT || 3001
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`)
  console.log('Listening for Stripe webhooks at /webhook')
})