Billing Portal
Let users manage their own subscriptions, update payment methods, and view invoices using Stripe's hosted billing portal.
Stripe's Billing Portal lets your users self-manage their subscriptions without you building custom UI. Users can update payment methods, change plans, cancel, and view invoices.
What Users Can Do
The billing portal allows users to:
- ✅ Update payment method (card, bank account)
- ✅ View and download invoices
- ✅ Cancel subscription
- ✅ View subscription details
- ✅ Update billing information
Hosted by Stripe
The billing portal is hosted entirely by Stripe. You just redirect users there - no UI to build or maintain.
Opening the Portal
Use the subscription.billingPortal() method to redirect users:
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { subscription } from "@/lib/auth-client";
import { stripeConfig } from "@/config/app";
export function ManageBillingButton() {
const [isLoading, setIsLoading] = useState(false);
const handleClick = async () => {
if (!subscription) return;
setIsLoading(true);
const { error } = await subscription.billingPortal({
returnUrl: stripeConfig.urls.billingReturn,
disableRedirect: false,
});
if (error) {
console.error(error);
setIsLoading(false);
}
// User is redirected to Stripe
};
return (
<Button onClick={handleClick} disabled={isLoading}>
{isLoading ? "Loading..." : "Manage Billing"}
</Button>
);
}Parameters
| Option | Type | Description |
|---|---|---|
returnUrl | string | Where to redirect after leaving portal |
locale | string | Portal language (e.g., "en", "de", "fr") |
disableRedirect | boolean | If true, returns URL instead of redirecting |
Return URL
Configure where users return after using the portal:
export const stripeConfig = {
urls: {
billingReturn: "/dashboard/billing",
},
// ...
};Billing Dashboard Page
Create a billing page that shows subscription status and portal access:
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { ManageBillingButton } from "@/components/billing-button";
import { getPlan, formatPrice } from "@/config/app";
export default async function BillingPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect("/sign-in");
}
// Get user's subscription from your database
const subscription = await getSubscription(session.user.id);
const plan = subscription ? getPlan(subscription.plan) : getPlan("free");
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Billing</h1>
<div className="rounded-lg border p-6">
<h2 className="text-lg font-semibold">Current Plan</h2>
<div className="mt-4 flex items-center justify-between">
<div>
<p className="text-2xl font-bold capitalize">{plan?.name}</p>
<p className="text-muted-foreground">
{plan?.priceId
? `${formatPrice(plan.displayPrice.monthly)}/month`
: "Free forever"
}
</p>
</div>
{subscription && (
<ManageBillingButton />
)}
</div>
{subscription?.status === "trialing" && (
<p className="mt-4 text-sm text-blue-600">
Trial ends on {new Date(subscription.trialEnd!).toLocaleDateString()}
</p>
)}
{subscription?.cancelAtPeriodEnd && (
<p className="mt-4 text-sm text-amber-600">
Cancels on {new Date(subscription.periodEnd!).toLocaleDateString()}
</p>
)}
</div>
{!subscription && (
<div className="rounded-lg border border-dashed p-6 text-center">
<p className="text-muted-foreground">
You're on the free plan.
</p>
<a href="/pricing" className="mt-2 inline-block text-primary underline">
Upgrade to unlock more features
</a>
</div>
)}
</div>
);
}Customizing the Portal
Configure the portal appearance in Stripe Dashboard:
- Go to Settings → Billing → Customer portal
- Configure which features are enabled
- Set your business information
- Add your logo and brand colors
Portal Settings
| Setting | Description |
|---|---|
| Invoice history | Let customers view past invoices |
| Customer information | Allow updating billing details |
| Payment methods | Allow adding/removing payment methods |
| Subscriptions | Allow canceling, switching plans |
| Cancellation reasons | Collect feedback on cancellation |
Subscription Actions via Portal
Users can perform these actions in the portal:
Cancel Subscription
The portal handles cancellation gracefully:
- Shows cancellation effective date
- Optionally collects cancellation reason
- Allows immediate or end-of-period cancellation
Restore Canceled Subscription
If a user cancels but it hasn't ended yet, they can restore it from the portal.
Update Payment Method
Users can:
- Add new cards
- Set default payment method
- Remove old payment methods
Alternative: In-App Actions
If you prefer handling some actions in your app instead of the portal:
Cancel in App
"use client";
import { subscription } from "@/lib/auth-client";
import { stripeConfig } from "@/config/app";
async function handleCancel() {
const { error } = await subscription.cancel({
returnUrl: stripeConfig.urls.billingReturn,
});
if (error) {
console.error(error);
}
// Redirects to Stripe portal's cancellation flow
}Restore in App
"use client";
import { subscription } from "@/lib/auth-client";
async function handleRestore() {
const { error } = await subscription.restore();
if (error) {
console.error(error);
return;
}
// Subscription restored!
window.location.reload();
}Success/Cancel Handling
Handle return from the portal:
import { Suspense } from "react";
function BillingNotifications({ searchParams }) {
const success = searchParams?.success;
const canceled = searchParams?.canceled;
return (
<>
{success && (
<div className="rounded-lg bg-green-50 p-4 text-green-800">
Your billing has been updated successfully!
</div>
)}
{canceled && (
<div className="rounded-lg bg-amber-50 p-4 text-amber-800">
Billing update was canceled.
</div>
)}
</>
);
}
export default function BillingPage({ searchParams }) {
return (
<div>
<Suspense fallback={null}>
<BillingNotifications searchParams={searchParams} />
</Suspense>
{/* Rest of billing page */}
</div>
);
}Testing the Portal
- Create a test subscription using a test card
- Click "Manage Billing" to open the portal
- Try updating payment method, viewing invoices, canceling
- Verify you're redirected back correctly
Test Mode Portal
In test mode, the portal works the same as production but won't process real payments.
Common Issues
"No customer portal configuration"
You haven't configured the portal in Stripe Dashboard. Go to Settings → Billing → Customer portal.
User Not Redirected Back
Check that returnUrl is a valid absolute URL or relative path that exists in your app.
Can't Cancel Subscription
Enable "Cancel subscriptions" in the portal settings in Stripe Dashboard.
Next Steps
Now you have a complete Stripe integration with:
- ✅ Subscription plans
- ✅ Checkout flow
- ✅ Trial periods
- ✅ Webhook handling
- ✅ Lifecycle emails
- ✅ Self-service billing portal
Consider adding:
- Usage-based billing for metered features
- Team/organization billing
- Multiple subscriptions per user