Portal
Generate token-based customer portal sessions for subscription and payment management
Create Portal Session
POST /api/billstack/teams/{teamId}/projects/{projectId}/portalGenerates a short-lived, token-based URL that gives a customer access to the self-hosted billing portal.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | BillStack customer ID (pc_...) |
ttlMs | number | No | Token time-to-live in milliseconds (default: 3600000 / 1 hour) |
Example:
curl -X POST "https://your-billstack.com/.../portal" \
-H "Authorization: Bearer bs_..." \
-H "Content-Type: application/json" \
-d '{
"customerId": "pc_a1b2c3d4",
"ttlMs": 3600000
}'Response (201):
{
"sessionId": "portal_sess_abc123",
"token": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"portalUrl": "https://your-billstack.com/portal/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"expiresAt": "2026-03-30T13:00:00Z"
}Redirect the customer to portalUrl.
Auth: Write access (session or API key with subscriptions:write).
Portal Page
GET /portal/{token}This is a rendered page (not an API endpoint). When a customer visits this URL:
- The token is validated server-side (checks expiration and customer existence)
- Customer data is loaded (subscriptions, invoices, payment methods)
- A self-hosted portal UI is rendered with three tabs
Subscriptions Tab
Displays all active subscriptions for the customer. Each subscription shows:
- Product name and price
- Status (active, trialing, past_due, etc.)
- Current billing period
- Cancel button — cancels at period end
- Resume button — reactivates a subscription set to cancel
Under the hood, actions call:
PATCH /api/portal/{token}/subscriptions/{subscriptionId}
Body: { "cancelAtPeriodEnd": true | false }Invoices Tab
Lists all invoices with:
- Invoice number and date
- Amount and status (paid, open, void)
- Download PDF link (served from Stripe)
Payment Methods Tab
Shows the customer's current payment method and a button to update it.
Update Payment Method calls:
POST /api/portal/{token}/setup-intentThis creates a Stripe Checkout session in setup mode and returns a URL. The customer is redirected to Stripe's secure page to enter new card details. No card data ever touches BillStack.
Response:
{
"url": "https://checkout.stripe.com/c/pay/cs_setup_xxx..."
}Security Model
| Property | Detail |
|---|---|
| Token lifetime | Configurable via ttlMs (default: 1 hour) |
| Token scope | Single customer only |
| Validation | Server-side on every request |
| Payment data | Never stored by BillStack — Stripe handles all card details |
| No login required | Token-based access — ideal for email links |
Integration Example
Generate portal links server-side and render a "Manage Billing" button in your SaaS app:
// Your SaaS app's API route
export async function POST(req: Request) {
const user = await getAuthenticatedUser(req);
const res = await fetch(`${BILLSTACK_URL}/.../portal`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customerId: user.billstackCustomerId,
}),
});
const { portalUrl } = await res.json();
return Response.json({ portalUrl });
}You can also include portal links in emails (e.g., "Manage your subscription") since they don't require authentication beyond the token.