Documentation

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

  1. Sign up at stripe.com
  2. Complete business verification
  3. 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:

  1. Go to Products → Add Product
  2. Create products for each plan (Free, Pro, Business)
  3. Add recurring prices (monthly and yearly)
  4. 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

$0/mo
  • ✓ 100 credits/month
  • ✓ Basic AI models
  • ✓ Community support
POPULAR

Pro

$19/mo
  • ✓ 1,000 credits/month
  • ✓ All AI models
  • ✓ Priority support
  • ✓ API access

Business

$49/mo
  • ✓ 5,000 credits/month
  • ✓ All AI models
  • ✓ Dedicated support
  • ✓ Custom integrations

Seeding Plans

Plans are automatically seeded when you run:

npx prisma db seed

Checkout 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 Stripe

3. 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/stripe

Webhook 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

  1. Go to Stripe Dashboard → Developers → Webhooks
  2. Click "Add endpoint"
  3. URL: https://yourdomain.com/api/webhooks/stripe
  4. Select events:
    • checkout.session.completed
    • customer.subscription.*
    • invoice.*
  5. 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

ScenarioCard Number
Success4242 4242 4242 4242
Decline4000 0000 0000 0002
3D Secure4000 0025 0000 3155
Insufficient Funds4000 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_succeeded

Best 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 session
  • POST /api/webhooks/stripe - Stripe webhooks
  • GET /api/subscriptions - List subscriptions
  • GET /api/payments - List payments