Billing & Subscriptions
Complete Stripe integration with subscription management, payment processing, and credit-based billing system.
Overview
The billing system uses Stripe for payment processing and subscription management. It includes a credit-based system where users can purchase subscription plans to receive monthly credits for AI usage.
Stripe Setup
1. Create Stripe Account
- Sign up at stripe.com
- Complete business verification
- Get your API keys from Dashboard → Developers → API keys
2. Configure Environment Variables
# Stripe API Keys
STRIPE_SECRET_KEY="sk_test_your_secret_key"
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_your_publishable_key"
STRIPE_WEBHOOK_SECRET="whsec_your_webhook_secret"
# Stripe Price IDs
STRIPE_PRO_MONTHLY_PRICE_ID="price_xxx"
STRIPE_PRO_YEARLY_PRICE_ID="price_xxx"
STRIPE_BUSINESS_MONTHLY_PRICE_ID="price_xxx"
STRIPE_BUSINESS_YEARLY_PRICE_ID="price_xxx"3. Create Products and Prices
In Stripe Dashboard:
- Go to Products → Add Product
- Create products for each plan (Free, Pro, Business)
- Add recurring prices (monthly and yearly)
- Copy Price IDs to environment variables
Subscription Plans
Database Schema
model Plan {
id String @id @default(cuid())
name String @unique
description String?
monthlyPrice Float
yearlyPrice Float
chatsPerMonth Int @default(-1) // -1 = unlimited
pdfsPerChat Int @default(-1)
creditsPerMonth Int @default(0)
features String[] // Array of features
isActive Boolean @default(true)
}Default Plans
Free
- ✓ 100 credits/month
- ✓ Basic AI models
- ✓ Community support
Pro
- ✓ 1,000 credits/month
- ✓ All AI models
- ✓ Priority support
- ✓ API access
Business
- ✓ 5,000 credits/month
- ✓ All AI models
- ✓ Dedicated support
- ✓ Custom integrations
Seeding Plans
Plans are automatically seeded when you run:
npx prisma db seedCheckout Flow
1. Create Checkout Session
Users click "Subscribe" button, which triggers:
// POST /api/create-checkout
{
"priceId": "price_xxx",
"billingCycle": "monthly" // or "yearly"
}
// Response
{
"url": "https://checkout.stripe.com/c/pay/cs_xxx"
}2. Redirect to Stripe Checkout
const response = await fetch("/api/create-checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
planId: selectedPlan.id,
billingCycle: "monthly",
}),
});
const { url } = await response.json();
window.location.href = url; // Redirect to Stripe3. Payment Processing
Stripe handles:
- Payment form (PCI compliant)
- Card validation
- 3D Secure authentication
- Payment processing
4. Webhook Processing
After successful payment, Stripe sends webhook to:
POST /api/webhooks/stripeWebhook Events
Handled Events
checkout.session.completed
Creates subscription and assigns credits when checkout succeeds
customer.subscription.created
Creates subscription record in database
customer.subscription.updated
Updates subscription status (active, past_due, canceled)
customer.subscription.deleted
Marks subscription as canceled
invoice.payment_succeeded
Records payment and renews credits for billing cycle
invoice.payment_failed
Marks subscription as past_due, sends notification
Webhook Setup
Local Development
# Install Stripe CLI
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Copy webhook signing secret to .env
STRIPE_WEBHOOK_SECRET="whsec_xxx"Production
- Go to Stripe Dashboard → Developers → Webhooks
- Click "Add endpoint"
- URL:
https://yourdomain.com/api/webhooks/stripe - Select events:
checkout.session.completedcustomer.subscription.*invoice.*
- Copy signing secret to production environment
Credit System
How Credits Work
- New users get 100 free credits
- Subscription plans include monthly credit allocations
- Credits are consumed when using AI features
- Credits reset at start of each billing cycle
Credit Consumption
// Deduct credits when user uses AI
await prisma.user.update({
where: { id: userId },
data: {
credits: {
decrement: creditsUsed,
},
},
});
// Track usage
await prisma.creditUsage.create({
data: {
userId,
amount: creditsUsed,
description: "AI Chat - GPT-4",
},
});Credit Renewal
Credits are automatically renewed on successful invoice payment:
// When invoice.payment_succeeded webhook received
const subscription = await prisma.subscription.findUnique({
where: { stripeSubscriptionId },
include: { plan: true },
});
await prisma.user.update({
where: { id: subscription.userId },
data: {
credits: subscription.plan.creditsPerMonth,
},
});Subscription Management
View Subscription
// GET /api/subscriptions
const subscription = await prisma.subscription.findUnique({
where: { userId: session.user.id },
include: { plan: true },
});Cancel Subscription
// DELETE /api/subscriptions/{id}
await stripe.subscriptions.update(stripeSubscriptionId, {
cancel_at_period_end: true,
});Change Plan
// Update subscription
await stripe.subscriptions.update(stripeSubscriptionId, {
items: [{
id: subscriptionItemId,
price: newPriceId,
}],
proration_behavior: 'create_prorations',
});Payment Records
Payment Model
model Payment {
id String @id @default(cuid())
userId String
stripePaymentId String @unique
amount Float
currency String
status String // succeeded, failed, pending
description String?
invoiceUrl String?
receiptUrl String?
createdAt DateTime @default(now())
}View Payment History
// GET /api/payments
const payments = await prisma.payment.findMany({
where: { userId: session.user.id },
orderBy: { createdAt: 'desc' },
});Refunds
Process Refund (Admin Only)
// Admin action
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
amount: amountInCents, // Optional: partial refund
reason: 'requested_by_customer',
});
// Update payment status
await prisma.payment.update({
where: { stripePaymentId },
data: { status: 'refunded' },
});Testing
Test Cards
| Scenario | Card Number |
|---|---|
| Success | 4242 4242 4242 4242 |
| Decline | 4000 0000 0000 0002 |
| 3D Secure | 4000 0025 0000 3155 |
| Insufficient Funds | 4000 0000 0000 9995 |
Use any future expiry date and any 3-digit CVC.
Test Webhooks Locally
# Start webhook listener
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Trigger test events
stripe trigger checkout.session.completed
stripe trigger invoice.payment_succeededBest Practices
Security
- Never expose secret keys in client-side code
- Always verify webhook signatures
- Use HTTPS in production
- Implement idempotency for webhook handlers
Error Handling
- Handle payment failures gracefully
- Notify users of failed payments
- Implement retry logic for failed webhooks
- Log all payment events for debugging
User Experience
- Show clear pricing information
- Display credit balance prominently
- Send email confirmations for payments
- Provide easy cancellation process
Troubleshooting
Webhook Not Receiving Events
- Verify webhook URL is accessible
- Check webhook secret is correct
- Review Stripe webhook logs in dashboard
- Ensure endpoint returns 200 status code
Payment Declined
- Check card has sufficient funds
- Verify card details are correct
- Test with Stripe test cards first
- Review Stripe dashboard for decline reason
Credits Not Updating
- Check webhook was received and processed
- Verify subscription is active in database
- Review credit renewal logic
- Check for database transaction errors
API Reference
See the API Documentation for billing endpoints:
POST /api/create-checkout- Create checkout sessionPOST /api/webhooks/stripe- Stripe webhooksGET /api/subscriptions- List subscriptionsGET /api/payments- List payments