Your customers need to manage subscriptions, update payment methods, and download invoices—but you don't want to build that UI. Stripe's Billing Portal handles all of this out of the box. Here's how to set it up.
Enable and Configure the Portal
Before you can use the Billing Portal, you need to enable it in your Stripe account and decide which features customers can access.
Navigate to Billing Portal Settings
In the Stripe Dashboard, go to Settings > Billing Portal. You'll see options to enable the portal and customize features.
Enable Portal Features
Turn on the features you want customers to access: Update payment method, Cancel subscription, View invoices, and Download invoices. You can also set branding to match your product with a logo and accent color.
Configure Subscription Management Options
Under Subscription updates, choose whether customers can pause, resume, or cancel subscriptions. Set allowed intervals for plan changes if you want to restrict how often they can upgrade or downgrade. Save your configuration.
// Portal configuration is managed via Dashboard, but you can also
// use the API to create a custom configuration programmatically:
const configuration = await stripe.billingPortal.configurations.create({
features: {
subscription_cancel: { enabled: true },
subscription_pause: { enabled: true },
subscription_update: { enabled: true, proration_behavior: 'create_prorations' },
invoice_history: { enabled: true },
payment_method_update: { enabled: true },
},
business_profile: {
privacy_policy_url: 'https://example.com/privacy',
terms_of_service_url: 'https://example.com/terms',
},
});Generate and Serve Portal Sessions
Once configured, you create a Billing Portal session server-side and send the link to your customer.
Install Stripe SDK
Add the Stripe Node SDK to your backend: npm install stripe or yarn add stripe. Initialize it with your secret key.
const stripe = require('stripe')('sk_live_...');
// Or using ES modules:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);Create a Billing Portal Session
Call stripe.billingPortal.sessions.create() with the customer ID and a return URL (where they go after exiting the portal). The method returns a session with a URL.
const session = await stripe.billingPortal.sessions.create({
customer: 'cus_1A2B3C4D5E6F7G8H', // The customer's Stripe ID
return_url: 'https://example.com/account', // Where they return after
});
console.log(session.url); // https://billing.stripe.com/p/session/...Redirect the Customer to the Portal
Send the session.url to your frontend and redirect the customer (or open in a new tab). They'll be authenticated automatically via the session token—no login required.
// Backend route (Node.js + Express example)
app.post('/create-portal-session', async (req, res) => {
const { customerId } = req.body;
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: 'https://example.com/account',
});
res.json({ url: session.url });
});
// Frontend: redirect on button click
fetch('/create-portal-session', { method: 'POST', body: JSON.stringify({ customerId }) })
.then(res => res.json())
.then(({ url }) => window.location.href = url);return_url must be a full URL. Stripe won't redirect to relative paths like /account.Test and Monitor Changes
Verify the portal works before going live and set up webhooks to catch subscription changes.
Test with Stripe Test Mode
Toggle to Test mode in the Stripe Dashboard (top-left corner). Create a test customer with a test subscription using card 4242 4242 4242 4242. Generate a portal session and walk through the entire flow—update payment method, view invoices, try pausing or canceling.
Monitor Subscription Changes via Webhooks
Set up a webhook listener for events like customer.subscription.updated, customer.subscription.deleted, and invoice.payment_failed. This syncs changes back to your database (e.g., revoke feature access when a customer cancels).
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === 'customer.subscription.updated') {
const subscription = event.data.object;
// Sync to your database: check if paused, canceled, or plan changed
}
if (event.type === 'customer.subscription.deleted') {
const subscription = event.data.object;
// Revoke access for this customer
}
res.json({received: true});
});customer.subscription.deleted events to revoke access immediately, rather than polling Stripe's API.Common Pitfalls
- Forgetting to enable features in Billing Portal settings — customers see an empty portal if no features are turned on.
- Using a relative URL for
return_url— it must be a full URL starting withhttps://. - Not syncing subscription changes via webhooks — your backend gets out of sync if customers cancel, pause, or change plans in the portal.
- Testing in live mode instead of test mode — accidentally charging real customers or modifying real subscriptions during development.
Wrapping Up
You now have a fully functional Billing Portal that lets customers manage their own subscriptions without you building custom pages. Track how often they use it—pause vs. cancel rates, plan changes—to understand churn drivers. If you want to track this automatically across tools, Product Analyst can help.