Stripe Integration: Subscription & Billing Guide
Hey everyone! π Let's dive into integrating Stripe for subscription billing. This guide walks you through setting up subscription tiers, handling payments, and managing customer subscriptions. This article will help you understand the core components, technical approaches, and best practices. Ready to get started? Let's go!
π‘ Overview: Stripe for Subscriptions
Stripe integration is key to a robust payment system. This setup will support monthly recurring subscriptions with three pricing tiers: Free, Pro, and Growth. Users can seamlessly upgrade their plans and manage their recurring payments. We're also making sure it handles any issues like payment failures, and gives customers a self-service portal for easy management. This ensures a smooth and efficient billing process. The aim is to create a reliable and user-friendly payment system that meets all requirements.
π οΈ Problem Statement: Addressing the Billing Needs
Okay, so what exactly are we trying to solve? We need a payment system that ticks all the boxes. First off, it needs to handle monthly recurring subscriptions. Then, we have usage limits per tier β like, if you're on the free plan, you get a certain number of emails per month. Crucially, it needs to handle plan upgrades and downgrades smoothly, and manage those pesky payment failures and retries gracefully. Finally, we want to give our users a customer portal so they can manage their billing info without any hassle.
π Proposed Solution: Integrating Stripe Checkout and Billing
Our solution? Integrate Stripe Checkout and Billing. This will enable us to offer three subscription tiers with different feature limits. We'll use hosted checkout pages for easy payment collection. Stripe will handle the heavy lifting while we get all the data through webhooks for subscription events. We'll also enforce usage limits and provide a customer portal so users can manage their plans. This approach ensures a streamlined and automated billing process.
βοΈ Technical Approach: Deep Dive into Implementation
Let's get into the nitty-gritty. We're structuring our system to handle pricing tiers, project components, subscription flows, and more.
π° Pricing Tiers: The Foundation
We'll have three tiers to cater to different user needs:
- Free: At $0/month, you get 20 emails per month and basic AI templates.
- Pro: For $29/month, you get unlimited emails/SMS and standard AI templates.
- Growth: This is the top tier at $49/month, offering unlimited outreach, advanced AI sequences, and analytics.
π Project Structure: Core Components
Hereβs how we're breaking down the project:
backend/
βββ app/
β βββ services/
β β βββ stripe_service.py # Stripe client wrapper
β β βββ subscription_service.py # Subscription management
β βββ api/v1/
β β βββ billing.py # Billing endpoints
β β βββ webhooks.py # Stripe webhook handler
β βββ models/
β β βββ subscription.py # Subscription model
β βββ middleware/
β βββ usage_limiter.py # Enforce tier limits
frontend/
βββ src/
β βββ app/
β β βββ billing/
β β β βββ page.tsx # Billing dashboard
β β β βββ checkout/page.tsx # Checkout flow
β β βββ pricing/
β β βββ page.tsx # Pricing page
β βββ components/
β βββ billing/
β βββ PricingCards.tsx
β βββ UsageIndicator.tsx
π Subscription Flow: Step-by-Step
Here's the sequence of events:
- Checkout: User clicks to upgrade.
- API Call: Frontend sends a POST request to the backend to create a checkout session.
- Stripe Session: The backend communicates with Stripe to create a checkout session.
- Redirection: Stripe returns a checkout URL, and the user is redirected to Stripe's hosted checkout.
- Payment: User enters payment details and completes the payment.
- Webhook: Stripe sends a
checkout.session.completedevent to our backend. - Subscription Record: Backend creates a subscription record in the database and updates the user's tier.
- Usage Enforcement: When the user sends an email campaign, the backend checks usage limits.
- Campaign Sent: If within limits, the campaign is sent, and the usage counter is incremented.
- Limit Exceeded: If the limit is exceeded, the user gets an error and is prompted to upgrade.
- Subscription Update: Stripe sends
invoice.payment_succeededon successful payments, resetting the monthly usage counter. - Cancellation: Users can cancel their subscription, and the backend marks the subscription as canceled.
sequenceDiagram
participant U as User
participant FE as Next.js
participant BE as FastAPI
participant S as Stripe API
participant DB as PostgreSQL
%% Checkout
U->>FE: Click "Upgrade to Pro"
FE->>BE: POST /api/v1/billing/create-checkout
BE->>S: Create Checkout Session
S-->>BE: Return checkout URL
BE-->>FE: Redirect URL
FE->>S: Redirect to Stripe Checkout
U->>S: Enter payment details
S->>U: Payment successful
S->>BE: Webhook: checkout.session.completed
BE->>DB: Create Subscription record
BE->>DB: Update User.subscription_tier
%% Usage Enforcement
U->>FE: Send email campaign
FE->>BE: POST /api/v1/campaigns/send
BE->>DB: Check usage limits
alt Within limits
BE->>DB: Increment usage counter
BE-->>FE: Campaign sent
else Limit exceeded
BE-->>FE: 403 Forbidden (upgrade required)
end
%% Subscription Update
S->>BE: Webhook: invoice.payment_succeeded
BE->>DB: Reset monthly usage counter
%% Cancellation
U->>FE: Cancel subscription
FE->>BE: POST /api/v1/billing/cancel
BE->>S: Cancel subscription
S-->>BE: Subscription canceled
BE->>DB: Mark subscription as canceled
β Acceptance Criteria: Ensuring Success
To make sure everything's working perfectly, we have several criteria to meet. These steps will guarantee a successful implementation of Stripe integration for our subscription and billing system.
π³ Stripe Setup: Configuration is Key
- [x] Create a Stripe account with both test and production API keys.
- [x] Create three subscription products in the Stripe Dashboard: Free, Pro, and Growth tiers.
- [x] Configure the webhook endpoint in the Stripe Dashboard.
- [x] Set up the webhook signing secret.
π» Backend Integration: Building the Core
- [x] Create a
Subscriptionmodel with key fields such asuser_id,stripe_customer_id,stripe_subscription_id,tier,status,current_period_start,current_period_end, andusage_count. - [x] Implement a Stripe client in
stripe_service.py. - [x] Create the checkout session endpoint:
POST /api/v1/billing/create-checkout. - [x] Create a customer portal endpoint:
POST /api/v1/billing/customer-portal. - [x] Build a webhook handler:
POST /api/v1/webhooks/stripe.
π‘ Webhook Events: Listening for Actions
- [x] Handle the
checkout.session.completedevent to create a new subscription. - [x] Handle the
invoice.payment_succeededevent to reset the usage counter. - [x] Handle
invoice.payment_failedto notify the user and retry the payment. - [x] Handle
customer.subscription.updatedto update the user's tier. - [x] Handle
customer.subscription.deletedto downgrade the user to the free tier.
β±οΈ Usage Enforcement: Keeping Things Fair
- [x] Create middleware to check usage limits before API actions.
- [x] Enforce the free tier limit of 20 emails per month.
- [x] Return a clear error message when the limit is exceeded.
- [x] Display the usage remaining in the dashboard.
- [x] Reset the counters at the beginning of each billing period.
π₯οΈ Frontend Pages: User-Friendly Interface
- [x] Build a pricing page with comparison cards for each tier.
- [x] Create the checkout flow using Stripe Checkout redirection.
- [x] Add a billing dashboard showing the current plan, usage for the month (emails sent/limit), the next billing date, and the payment method.
- [x] Add a βManage Billingβ button to open the Stripe Customer Portal.
- [x] Show an upgrade prompt when the usage limit is reached.
πͺ Customer Portal: Empowering Users
- [x] Enable the Stripe Customer Portal in the dashboard.
- [x] Configure portal features: update payment method, view invoices, cancel subscriptions, and upgrade/downgrade plans.
- [x] Redirect users to the portal from within the app.
π¨ Error Handling: Dealing with Issues
- [x] Handle failed payments gracefully.
- [x] Notify users of upcoming renewals.
- [x] Send dunning emails for failed payments.
- [x] Implement a grace period for payment failures.
- [x] Log all webhook events for easy debugging.
π§ͺ Testing: Ensuring Quality
- [x] Test the checkout flow in Stripe test mode.
- [x] Test webhook delivery using the Stripe CLI.
- [x] Test usage limit enforcement.
- [x] Test subscription upgrades and downgrades.
- [x] Test payment failure scenarios.
- [x] Verify that the usage counter resets correctly.
π Success Metrics: Measuring Performance
We'll measure success by several key metrics. High performance here means our Stripe integration is working flawlessly.
- Checkout Completion Rate: Aim for over 70%.
- Payment Success Rate: Target over 95%.
- Billing Errors: Strive for zero billing errors in production.
- Webhook Processing Time: Ensure webhook processing is under 2 seconds.
- Usage Tracking Accuracy: Achieve 100% accuracy in usage tracking.
π Dependencies & Prerequisites: What You Need
Hereβs what you need to get this done:
Depends on:
- Issue #2 (Backend Core Infrastructure) - API structure.
- Issue #4 (Frontend Foundation) - Next.js app.
- Issue #7 (Authentication) - User sessions.
Required:
- Stripe account (test and production keys).
- SSL certificate for the webhook endpoint.
π Implementation Notes: Code Snippets & Examples
backend/app/models/subscription.py
Hereβs the Subscription model in Python. This model stores crucial information about each user's subscription, including the tier, status, and usage count.
from sqlalchemy import Column, String, Integer, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import func
import uuid
from app.database import Base
class Subscription(Base):
__tablename__ = "subscriptions"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
stripe_customer_id = Column(String, unique=True)
stripe_subscription_id = Column(String, unique=True)
tier = Column(String, default="free") # free | pro | growth
status = Column(String, default="active") # active | canceled | past_due
current_period_start = Column(DateTime(timezone=True))
current_period_end = Column(DateTime(timezone=True))
usage_count = Column(Integer, default=0) # Emails sent this period
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
backend/app/services/stripe_service.py
This StripeService handles all the interactions with the Stripe API, like creating checkout sessions, and customer portal sessions, and canceling subscriptions.
import stripe
from app.config import settings
stripe.api_key = settings.STRIPE_SECRET_KEY
class StripeService:
TIER_PRICE_IDS = {
"pro": settings.STRIPE_PRO_PRICE_ID,
"growth": settings.STRIPE_GROWTH_PRICE_ID
}
@staticmethod
def create_checkout_session(
user_id: str,
user_email: str,
tier: str,
success_url: str,
cancel_url: str
) -> str:
"""Create Stripe Checkout session"""
session = stripe.checkout.Session.create(
customer_email=user_email,
line_items=[{
"price": StripeService.TIER_PRICE_IDS[tier],
"quantity": 1
}],
mode="subscription",
success_url=success_url,
cancel_url=cancel_url,
metadata={"user_id": user_id}
)
return session.url
@staticmethod
def create_customer_portal_session(
stripe_customer_id: str,
return_url: str
) -> str:
"""Create Customer Portal session"""
session = stripe.billing_portal.Session.create(
customer=stripe_customer_id,
return_url=return_url
)
return session.url
@staticmethod
def cancel_subscription(stripe_subscription_id: str):
"""Cancel subscription at period end"""
stripe.Subscription.modify(
stripe_subscription_id,
cancel_at_period_end=True
)
backend/app/api/v1/webhooks.py
This file is crucial for handling webhook events from Stripe. It listens for events like successful payments and subscription updates and then updates your database accordingly.
from fastapi import APIRouter, Request, HTTPException
import stripe
from app.config import settings
from app.database import SessionLocal
from app.models.subscription import Subscription
from app.models.user import User
router = APIRouter()
@router.post("/stripe")
async def stripe_webhook(request: Request):
"""Handle Stripe webhook events"""
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
try:
event = stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid payload")
except stripe.error.SignatureVerificationError:
raise HTTPException(status_code=400, detail="Invalid signature")
db = SessionLocal()
try:
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
user_id = session["metadata"]["user_id"]
# Create subscription record
subscription = Subscription(
user_id=user_id,
stripe_customer_id=session["customer"],
stripe_subscription_id=session["subscription"],
tier="pro", # Determine from price_id
status="active",
usage_count=0
)
db.add(subscription)
# Update user tier
user = db.query(User).filter(User.id == user_id).first()
user.subscription_tier = "pro"
db.commit()
elif event["type"] == "invoice.payment_succeeded":
invoice = event["data"]["object"]
subscription_id = invoice["subscription"]
# Reset usage counter
subscription = db.query(Subscription).filter(
Subscription.stripe_subscription_id == subscription_id
).first()
if subscription:
subscription.usage_count = 0
db.commit()
# Handle other events...
finally:
db.close()
return {"status": "success"}
frontend/src/app/pricing/page.tsx
Hereβs a sample pricing page in TypeScript. It shows the different tiers and their features.
export default function PricingPage() {
const tiers = [
{
name: "Free",
price: "$0",
features: ["20 emails/month", "Basic AI templates", "Contact discovery"]
},
{
name: "Pro",
price: "$29",
features: ["Unlimited emails", "Unlimited SMS", "Standard AI templates"],
popular: true
},
{
name: "Growth",
price: "$49",
features: ["Everything in Pro", "Advanced AI sequences", "Analytics dashboard"]
}
]
return (
<div className="max-w-6xl mx-auto py-16">
<h1 className="text-4xl font-bold text-center mb-12">
Simple, Transparent Pricing
</h1>
<div className="grid md:grid-cols-3 gap-8">
{tiers.map(tier => (
<div key={tier.name} className="border rounded-lg p-6">
<h3 className="text-xl font-semibold">{tier.name}</h3>
<p className="text-3xl font-bold mt-4">{tier.price}<span className="text-lg">/mo</span></p>
<ul className="mt-6 space-y-3">
{tier.features.map(feature => (
<li key={feature}>β {feature}</li>
))}
</ul>
<button className="w-full mt-8 bg-blue-600 text-white py-2 rounded hover:bg-blue-700">
{tier.name === "Free" ? "Get Started" : "Upgrade"}
</button>
</div>
))}
</div>
</div>
)
}
π References & Research: Resources for Further Reading
Want to dive deeper? Check out these resources:
π Documentation
- Stripe Subscriptions: Provides an in-depth overview of Stripe's subscription features. Stripe Subscriptions
- Stripe Checkout: Details how to implement Stripe Checkout for payments. Stripe Checkout
- Stripe Webhooks: Explains how to set up and use webhooks to receive event notifications from Stripe. Stripe Webhooks
- Stripe Customer Portal: Guides you through implementing and using the Customer Portal for customer self-service. Stripe Customer Portal
π Architecture Reference
- /scripts/PRD.txt: Section 4.7 (Billing).
- /scripts/system_architecture_diagram.txt: Stripe integration.
β Best Practices
- Webhook security and verification: Ensuring secure and verified webhook handling.
- Idempotent webhook handling: Implementing idempotent webhook handling to prevent duplicate processing.
- Subscription lifecycle management: Best practices for managing the subscription lifecycle.
Estimate: This project should take roughly 3-4 days to implement, but this is an estimate.
That's it, guys! π₯³ Now you know how to integrate Stripe for subscription and billing. This guide covers everything from the setup to the technical aspects. Hope it was helpful. Feel free to reach out with any questions! Happy coding! π