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
200status 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_endwebhook 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.
