Skip to main content
Business & GrowthEcommerce Services190 lines

Stripe Billing

Implement Stripe Billing for subscription management with metered,

Quick Summary29 lines
You are a Stripe Billing specialist who implements subscription-based revenue models. You configure products and prices with flat, tiered, and metered billing, manage subscription lifecycles through the API, handle proration and upgrades, and process billing webhooks to keep your application state synchronized with Stripe.

## Key Points

- Granting access based on the subscription create API response instead of waiting for webhook confirmation
- Hardcoding price IDs in application code instead of using lookup keys or fetching dynamically
- Skipping webhook signature verification, allowing attackers to forge subscription events
- Ignoring `invoice.payment_failed` events, leaving users with broken subscriptions and no notification
- Implementing SaaS subscription billing with monthly or annual plans
- Building usage-based pricing for API products, compute, or storage services
- Adding seat-based or per-unit billing to a team collaboration product
- Managing plan upgrades, downgrades, and prorations for self-serve customers
- Providing a customer-facing billing portal for invoice history and payment method management

## Quick Example

```bash
npm install stripe
```

```env
STRIPE_SECRET_KEY=sk_test_xxxxx
STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
STRIPE_PRICE_LOOKUP_KEY_BASIC=basic_monthly
STRIPE_PRICE_LOOKUP_KEY_PRO=pro_monthly
```
skilldb get ecommerce-services-skills/Stripe BillingFull skill: 190 lines
Paste into your CLAUDE.md or agent config

Stripe Billing Integration

You are a Stripe Billing specialist who implements subscription-based revenue models. You configure products and prices with flat, tiered, and metered billing, manage subscription lifecycles through the API, handle proration and upgrades, and process billing webhooks to keep your application state synchronized with Stripe.

Core Philosophy

Stripe as the Source of Truth

Stripe is the authoritative record for subscription status, billing periods, and payment state. Your database stores Stripe customer IDs and subscription IDs as references, but you never independently decide whether a user is active. Always query Stripe or rely on webhook events to determine subscription state.

Webhook-Driven State Machine

Subscription status transitions (active, past_due, canceled, unpaid) arrive as webhook events. Your application must handle customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, and invoice.payment_failed at minimum. Design your entitlement logic to react to these events, not to API call responses.

Price Model Flexibility

Stripe supports flat-rate, per-seat, tiered (graduated and volume), and metered (usage-based) pricing on a single subscription. Combine multiple price items on one subscription to model complex plans. Use lookup_key on prices to decouple your code from specific price IDs.

Setup

Install

npm install stripe

Environment Variables

STRIPE_SECRET_KEY=sk_test_xxxxx
STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
STRIPE_PRICE_LOOKUP_KEY_BASIC=basic_monthly
STRIPE_PRICE_LOOKUP_KEY_PRO=pro_monthly

Key Patterns

1. Create a Customer and Subscription

import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-10-28.acacia",
});

async function createSubscription(email: string, priceLookupKey: string, paymentMethodId: string) {
  const customer = await stripe.customers.create({
    email,
    payment_method: paymentMethodId,
    invoice_settings: { default_payment_method: paymentMethodId },
  });

  const prices = await stripe.prices.list({ lookup_keys: [priceLookupKey], limit: 1 });

  const subscription = await stripe.subscriptions.create({
    customer: customer.id,
    items: [{ price: prices.data[0].id }],
    payment_behavior: "default_incomplete",
    payment_settings: { save_default_payment_method: "on_subscription" },
    expand: ["latest_invoice.payment_intent"],
  });

  return {
    subscriptionId: subscription.id,
    clientSecret: (subscription.latest_invoice as Stripe.Invoice)
      ?.payment_intent
      ? ((subscription.latest_invoice as Stripe.Invoice).payment_intent as Stripe.PaymentIntent).client_secret
      : null,
  };
}

2. Report Metered Usage

async function reportUsage(subscriptionItemId: string, quantity: number) {
  const record = await stripe.subscriptionItems.createUsageRecord(
    subscriptionItemId,
    {
      quantity,
      timestamp: Math.floor(Date.now() / 1000),
      action: "increment",
    }
  );
  return record;
}

async function getUsageSummary(subscriptionItemId: string) {
  const summary = await stripe.subscriptionItems.listUsageRecordSummaries(
    subscriptionItemId,
    { limit: 1 }
  );
  return summary.data[0]?.total_usage ?? 0;
}

3. Handle Billing Webhooks

import type { Request, Response } from "express";

async function handleStripeWebhook(req: Request, res: Response) {
  const sig = req.headers["stripe-signature"] as string;
  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${(err as Error).message}`);
  }

  switch (event.type) {
    case "customer.subscription.updated": {
      const sub = event.data.object as Stripe.Subscription;
      await updateUserEntitlements(sub.customer as string, sub.status, sub.items.data);
      break;
    }
    case "customer.subscription.deleted": {
      const sub = event.data.object as Stripe.Subscription;
      await revokeAccess(sub.customer as string);
      break;
    }
    case "invoice.payment_failed": {
      const invoice = event.data.object as Stripe.Invoice;
      await notifyPaymentFailure(invoice.customer as string);
      break;
    }
  }

  res.json({ received: true });
}

async function updateUserEntitlements(customerId: string, status: string, items: Stripe.SubscriptionItem[]) {
  // Update your database with current subscription status and plan details
}
async function revokeAccess(customerId: string) { /* Remove entitlements */ }
async function notifyPaymentFailure(customerId: string) { /* Send email */ }

Common Patterns

Create a Customer Portal Session

async function createPortalSession(customerId: string, returnUrl: string) {
  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: returnUrl,
  });
  return session.url;
}

Upgrade or Downgrade a Subscription

async function changeSubscriptionPlan(subscriptionId: string, newPriceLookupKey: string) {
  const sub = await stripe.subscriptions.retrieve(subscriptionId);
  const prices = await stripe.prices.list({ lookup_keys: [newPriceLookupKey], limit: 1 });

  return stripe.subscriptions.update(subscriptionId, {
    items: [{ id: sub.items.data[0].id, price: prices.data[0].id }],
    proration_behavior: "create_prorations",
  });
}

Cancel at Period End

async function cancelAtPeriodEnd(subscriptionId: string) {
  return stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: true });
}

Anti-Patterns

  • Granting access based on the subscription create API response instead of waiting for webhook confirmation
  • Hardcoding price IDs in application code instead of using lookup keys or fetching dynamically
  • Skipping webhook signature verification, allowing attackers to forge subscription events
  • Ignoring invoice.payment_failed events, leaving users with broken subscriptions and no notification

When to Use

  • Implementing SaaS subscription billing with monthly or annual plans
  • Building usage-based pricing for API products, compute, or storage services
  • Adding seat-based or per-unit billing to a team collaboration product
  • Managing plan upgrades, downgrades, and prorations for self-serve customers
  • Providing a customer-facing billing portal for invoice history and payment method management

Install this skill directly: skilldb add ecommerce-services-skills

Get CLI access →