6. Customer Portal
Replace Stripe's Customer Portal with BillStack's self-hosted portal
What the Portal Provides
BillStack includes a self-hosted customer portal at /portal/[token] that provides:
- Subscription management — view active subscriptions, cancel at period end, resume canceled subscriptions
- Invoice history — view and download PDF invoices
- Payment methods — add or update payment methods via Stripe's secure Setup mode
Unlike Stripe's Customer Portal, the BillStack portal is fully self-hosted, customizable, and project-scoped.
How Portal Access Works
Portal access is token-based with no customer login required:
- Your SaaS app calls the BillStack portal API to generate a session token
- BillStack returns a one-time URL with the token embedded
- You redirect your customer to that URL
- The token is validated server-side — it includes the customer ID and expiration
- The portal renders the customer's data directly
Generate a Portal Session
Before — Stripe Customer Portal:
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const session = await stripe.billingPortal.sessions.create({
customer: 'cus_xxx',
return_url: 'https://myapp.com/account',
});
// Redirect to session.urlAfter — BillStack Portal:
const response = await fetch(
`${BILLSTACK_URL}/api/billstack/teams/${TEAM_ID}/projects/${PROJECT_ID}/portal`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customerId: 'pc_xxx', // BillStack customer ID
ttlMs: 3600000, // optional: 1 hour (default)
}),
}
);
const { sessionId, token, portalUrl, expiresAt } = await response.json();
// Redirect to portalUrlResponse:
{
"sessionId": "portal_sess_abc123",
"token": "a1b2c3d4e5f6...",
"portalUrl": "https://your-billstack.com/portal/a1b2c3d4e5f6...",
"expiresAt": "2026-03-30T13:00:00Z"
}Portal Features
Subscription Management
Customers can view their active subscriptions and:
- Cancel at period end — subscription remains active until the billing period ends
- Resume — reactivate a subscription that was set to cancel at period end
These actions call PATCH /api/portal/[token]/subscriptions/[subscriptionId] with { cancelAtPeriodEnd: true/false }.
Invoice History
Customers see a list of their past invoices with:
- Invoice number and date
- Amount and status (paid, open, void)
- Download PDF links (direct from Stripe)
Payment Methods
Customers can update their payment method:
- Click Update Payment Method
- BillStack calls
POST /api/portal/[token]/setup-intentto create a Stripe Setup Intent - The customer is redirected to Stripe's secure checkout in setup mode
- After completing, the new payment method is attached to their Stripe customer
Integration Pattern
A typical integration in your SaaS app:
// In your SaaS app's account/billing page
export default function BillingPage() {
async function openPortal() {
const res = await fetch('/api/billing/portal', { method: 'POST' });
const { portalUrl } = await res.json();
window.location.href = portalUrl;
}
return (
<button onClick={openPortal}>
Manage Billing
</button>
);
}// /api/billing/portal route in your SaaS app
export async function POST(req: Request) {
const user = await getAuthenticatedUser(req);
const res = await fetch(
`${BILLSTACK_URL}/api/billstack/teams/${TEAM_ID}/projects/${PROJECT_ID}/portal`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customerId: user.billstackCustomerId,
}),
}
);
return Response.json(await res.json());
}Security
- Portal tokens are short-lived (default: 1 hour, configurable via
ttlMs) - Tokens are validated server-side on every request
- Customers can only see and manage their own data
- Payment method updates go through Stripe's secure hosted page — card details never touch BillStack
Next Step
The core migration is complete. Optionally, set up the referral system to grow your customer base.