Stripe Customer Portal: Self-Service Billing Without Building It
Building subscription management — upgrade, downgrade, cancel, update card — takes weeks. Stripe Customer Portal does all of it in an afternoon.
What the Portal Provides
- Update payment method
- View billing history and download invoices
- Cancel subscription
- Upgrade/downgrade plans
- Update billing address
All Stripe-hosted, fully branded to your business.
Setup: One-Time Configuration
// Create portal configuration (run once)
const configuration = await stripe.billingPortal.configurations.create({
business_profile: {
headline: 'Manage your Whoff Agents subscription',
privacy_policy_url: 'https://whoffagents.com/privacy',
terms_of_service_url: 'https://whoffagents.com/terms',
},
features: {
payment_method_update: { enabled: true },
subscription_cancel: {
enabled: true,
mode: 'at_period_end', // Don't cancel immediately
},
subscription_update: {
enabled: true,
default_allowed_updates: ['price'],
proration_behavior: 'create_prorations',
},
invoice_history: { enabled: true },
},
});
// Save configuration.id to env: STRIPE_PORTAL_CONFIG_ID
Create Portal Session
// app/api/billing/portal/route.ts
import { auth } from '@/auth';
import { stripe } from '@/lib/stripe';
import { db } from '@/db';
import { redirect } from 'next/navigation';
export async function POST(request: Request) {
const session = await auth();
if (!session) return new Response('Unauthorized', { status: 401 });
const user = await db.user.findUnique({
where: { id: session.user.id },
select: { stripeCustomerId: true },
});
if (!user?.stripeCustomerId) {
return new Response('No Stripe customer found', { status: 400 });
}
const portalSession = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
configuration: process.env.STRIPE_PORTAL_CONFIG_ID,
return_url: `${process.env.NEXT_PUBLIC_URL}/dashboard/billing`,
});
return Response.redirect(portalSession.url);
}
Billing Page Button
// app/dashboard/billing/page.tsx
export default async function BillingPage() {
const session = await auth();
const user = await getUser(session.user.id);
return (
<div>
<h1>Billing</h1>
<p>Current plan: <strong>{user.plan}</strong></p>
<form action='/api/billing/portal' method='POST'>
<button type='submit'>Manage Subscription</button>
</form>
</div>
);
}
Handle Portal Events via Webhooks
switch (event.type) {
case 'customer.subscription.updated':
const sub = event.data.object;
await db.user.update({
where: { stripeCustomerId: sub.customer as string },
data: { plan: sub.items.data[0].price.lookup_key ?? 'free' },
});
break;
case 'customer.subscription.deleted':
await db.user.update({
where: { stripeCustomerId: event.data.object.customer as string },
data: { plan: 'free' },
});
break;
}
Stripe Customer Portal ships pre-configured in the AI SaaS Starter Kit — portal API route, billing page, and webhook handlers all included. $99 at whoffagents.com.
Top comments (0)