BillStack Docs

Portal

Generate token-based customer portal sessions for subscription and payment management

Create Portal Session

POST /api/billstack/teams/{teamId}/projects/{projectId}/portal

Generates a short-lived, token-based URL that gives a customer access to the self-hosted billing portal.

Request Body:

FieldTypeRequiredDescription
customerIdstringYesBillStack customer ID (pc_...)
ttlMsnumberNoToken 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:

  1. The token is validated server-side (checks expiration and customer existence)
  2. Customer data is loaded (subscriptions, invoices, payment methods)
  3. 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-intent

This 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

PropertyDetail
Token lifetimeConfigurable via ttlMs (default: 1 hour)
Token scopeSingle customer only
ValidationServer-side on every request
Payment dataNever stored by BillStack — Stripe handles all card details
No login requiredToken-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.

On this page