Stripe Integration Guide for SaaS MVPs: Subscriptions, Usage Billing, and More

Stripe powers 80%+ of SaaS billing. Here is the complete integration guide for subscriptions, usage billing, trials, and webhooks.

Cover Image for Stripe Integration Guide for SaaS MVPs: Subscriptions, Usage Billing, and More

Stripe processes over $1 trillion in payments annually. More than 80% of SaaS startups use Stripe for billing. There's a reason: Stripe's API is the best-designed payment API in the world, and their subscription infrastructure handles the billing complexity that would take months to build yourself.

But "integrate Stripe" is deceptively simple advice. The difference between a Stripe integration that works and one that handles edge cases — failed payments, plan changes, proration, dunning, tax compliance — is hundreds of hours of development time.

We've integrated Stripe into multiple SaaS products, including AeroCopilot (aviation subscriptions with regulatory compliance requirements). Here's the complete guide.

Choosing Your Billing Model

Before writing code, decide how you'll charge. Stripe supports several models, and your choice affects your entire integration architecture.

Flat-Rate Subscriptions

The simplest model. Users pay a fixed monthly or annual price for access to a plan tier. Most B2B SaaS products start here.

  • Free tier → limited features, usage caps
  • Pro tier → $29/month, full features, higher limits
  • Enterprise tier → $99/month, team features, priority support

This model works when your cost to serve each customer is roughly equal. Content platforms, project management tools, and CRM products typically use flat-rate pricing.

Usage-Based Billing (Metered)

Users pay based on consumption — API calls, storage used, messages sent, compute minutes. Stripe's metered billing tracks usage and invoices automatically.

This model works when your cost to serve varies significantly per customer. AI products, infrastructure tools, and communication platforms (like Twilio) use usage-based pricing because a customer making 100 API calls costs dramatically less to serve than one making 1,000,000.

Hybrid (Flat + Usage)

A base subscription fee plus usage charges above included limits. This is increasingly common in 2026 — it provides predictable revenue while aligning cost with value.

Example: $49/month includes 10,000 API calls. Additional calls at $0.005 each. This gives customers predictable costs at normal usage while ensuring heavy users pay proportionally.

Per-Seat Pricing

Price scales with the number of users on the account. Common in B2B SaaS where the product's value increases with team adoption. Stripe handles per-seat billing through quantity-based subscriptions.

Setting Up Your Stripe Integration

Product and Price Configuration

Stripe separates Products (what you sell) from Prices (how you charge). A single product can have multiple prices — monthly, annual, metered, one-time.

// Create products and prices in your seed script or Stripe dashboard
const product = await stripe.products.create({
  name: 'Pro Plan',
  description: 'Full access to all features',
  metadata: { plan_tier: 'pro' }
})

const monthlyPrice = await stripe.prices.create({
  product: product.id,
  unit_amount: 2900, // $29.00 in cents
  currency: 'usd',
  recurring: { interval: 'month' },
  metadata: { billing_cycle: 'monthly' }
})

const annualPrice = await stripe.prices.create({
  product: product.id,
  unit_amount: 29000, // $290.00 — two months free
  currency: 'usd',
  recurring: { interval: 'year' },
  metadata: { billing_cycle: 'annual' }
})

Best practice: Define products and prices in the Stripe Dashboard for your production environment, then reference them by ID in your application. Hardcoding price creation in application code leads to duplicate products.

Customer Creation and Sync

Every user in your system should map to a Stripe Customer. Create the customer when the user signs up, and store the stripe_customer_id in your database.

const customer = await stripe.customers.create({
  email: user.email,
  name: user.name,
  metadata: {
    user_id: user.id,
    tenant_id: user.tenantId // for multi-tenant SaaS
  }
})

// Store in your database
await prisma.user.update({
  where: { id: user.id },
  data: { stripeCustomerId: customer.id }
})

Critical: Always store the mapping between your user and the Stripe customer. Losing this mapping means losing the ability to manage their subscription.

Checkout Sessions

Stripe Checkout is a hosted payment page that handles card input, validation, 3D Secure, and PCI compliance. Use it instead of building custom payment forms.

const session = await stripe.checkout.sessions.create({
  customer: user.stripeCustomerId,
  mode: 'subscription',
  line_items: [{
    price: priceId,
    quantity: 1,
  }],
  success_url: `${baseUrl}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${baseUrl}/pricing`,
  subscription_data: {
    trial_period_days: 14,
    metadata: { tenant_id: tenantId }
  },
  allow_promotion_codes: true,
})

Stripe Checkout handles PCI compliance for you. Building a custom payment form means you're responsible for PCI SAQ A-EP compliance at minimum — a significant security and audit burden for an MVP.

Webhook Handling: The Critical Infrastructure

Webhooks are how Stripe tells your application what happened. A customer paid. A payment failed. A subscription was cancelled. A trial is expiring. If your webhook handler is broken, your billing is broken.

Essential Webhooks for SaaS

At minimum, handle these events:

switch (event.type) {
  case 'checkout.session.completed':
    // Activate subscription, provision access
    break
  case 'customer.subscription.updated':
    // Handle plan changes, status changes
    break
  case 'customer.subscription.deleted':
    // Revoke access, clean up resources
    break
  case 'invoice.payment_succeeded':
    // Record payment, extend access
    break
  case 'invoice.payment_failed':
    // Notify user, start grace period
    break
  case 'customer.subscription.trial_will_end':
    // Send conversion email 3 days before trial ends
    break
}

Idempotency: Handle Duplicates Gracefully

Stripe may send the same webhook event multiple times. Your handler must be idempotent — processing the same event twice should produce the same result as processing it once.

async function handleWebhook(event: Stripe.Event) {
  // Check if we've already processed this event
  const existing = await prisma.stripeEvent.findUnique({
    where: { stripeEventId: event.id }
  })
  if (existing) return // Already processed

  // Process the event
  await processEvent(event)

  // Record that we've processed it
  await prisma.stripeEvent.create({
    data: {
      stripeEventId: event.id,
      type: event.type,
      processedAt: new Date()
    }
  })
}

Webhook Security

Always verify webhook signatures. Stripe signs every webhook with your endpoint secret. Never trust unverified webhooks.

const event = stripe.webhooks.constructEvent(
  body,
  signature,
  process.env.STRIPE_WEBHOOK_SECRET!
)

Retry Logic

Stripe retries failed webhook deliveries for up to 3 days with exponential backoff. Your endpoint must:

  • Return a 200 status within 20 seconds
  • Process heavy work asynchronously (queue the event, return 200, process later)
  • Log failures with enough context to debug

Trial Management

Trials are the primary conversion mechanism for SaaS. Get them right.

  • 14-day trials are standard. Long enough to evaluate, short enough to create urgency.
  • No credit card required trials get more signups but lower conversion. Card-required trials get fewer signups but higher conversion. Test both.
  • Send emails at key points: trial start (welcome + quick wins), day 7 (check-in + advanced features), day 11 (trial ending soon), day 14 (trial ended + special offer)
  • Use customer.subscription.trial_will_end webhook to trigger the final conversion sequence

Customer Portal

Stripe's Customer Portal lets users manage their own billing — update payment methods, change plans, view invoices, cancel subscriptions. Embed it with a single API call.

const portalSession = await stripe.billingPortal.sessions.create({
  customer: user.stripeCustomerId,
  return_url: `${baseUrl}/dashboard/settings`,
})

// Redirect user to portalSession.url

This saves you from building billing management UI — which is surprisingly complex when you account for proration previews, payment method updates, invoice history, and cancellation flows.

Handling Plan Changes and Proration

When users upgrade or downgrade mid-cycle, proration determines how much they owe.

  • Upgrade: Stripe charges the prorated difference immediately (or on the next invoice)
  • Downgrade: Stripe credits the unused portion of the current plan and applies it to the new plan
  • Configure proration behavior based on your business model:
await stripe.subscriptions.update(subscriptionId, {
  items: [{
    id: subscriptionItemId,
    price: newPriceId,
  }],
  proration_behavior: 'create_prorations', // or 'none', 'always_invoice'
})

Tax Compliance with Stripe Tax

Stripe Tax automatically calculates and collects sales tax, VAT, and GST based on the customer's location. In 2026, with expanding digital services tax requirements globally, this is no longer optional.

const session = await stripe.checkout.sessions.create({
  // ... other params
  automatic_tax: { enabled: true },
})

Enable Stripe Tax from launch. Retroactively calculating and remitting uncollected taxes is painful, expensive, and potentially illegal.

Usage-Based Billing Implementation

For metered billing, report usage to Stripe and let them handle invoicing.

// Report usage events throughout the billing period
await stripe.subscriptionItems.createUsageRecord(
  subscriptionItemId,
  {
    quantity: apiCallCount,
    timestamp: Math.floor(Date.now() / 1000),
    action: 'increment', // or 'set' for absolute values
  }
)

Best practice: Batch usage reports. Don't call the Stripe API on every single user action — aggregate usage in your database and report to Stripe hourly or daily.

For AeroCopilot, we implemented usage tracking for flight plan generations and METAR decodings — core product actions that map directly to the value delivered. Users on the free tier get a limited number of decodings per month; paid plans include higher or unlimited usage.

Invoice Management

Stripe generates invoices automatically for subscriptions. Customize them for professionalism:

  • Add your company logo and branding in Stripe Dashboard → Settings → Branding
  • Include your business address and tax ID
  • Customize invoice email templates
  • Set payment terms (net 15, net 30) for enterprise customers
  • Enable automatic invoice finalization

Common Pitfalls

1. Not handling failed payments. 10-15% of subscription renewals fail due to expired cards, insufficient funds, or bank declines. Implement Stripe's Smart Retries and send dunning emails.

2. Ignoring webhook ordering. Events can arrive out of order. Don't assume invoice.payment_succeeded arrives before customer.subscription.updated. Design handlers to work regardless of order.

3. Testing only the happy path. Test failed payments, disputed charges, subscription cancellations, and plan changes. Use Stripe's test clocks to simulate time-based scenarios.

4. Hardcoding prices. Store Stripe Price IDs in environment variables or your database, not in application code. Prices change. Plans evolve. Your code shouldn't need deployment to update pricing.

5. Forgetting about refunds. Build a refund workflow before you need one. Handle charge.refunded webhooks to revoke access or adjust usage credits.

The Bottom Line

Stripe integration isn't a weekend project. A production-grade billing system — with trials, upgrades, downgrades, usage billing, tax compliance, dunning, and customer portal — takes 2-4 weeks of focused development. But it's development that pays for itself immediately.

Get billing right from launch. Retrofitting billing logic into a running product — with real customers, real subscriptions, and real money — is exponentially harder than building it correctly from the start.

The psychology of SaaS pricing determines what you charge. Stripe determines how you collect it. Get both right and revenue follows.