Billing portal
Let customers manage their own plan, card and invoices — on a page Base316 hosts, or with one React component in yours.
Every subscription product ends up building the same screen: what plan am I on, what will I be charged next, change my plan, update my card, download last month's invoice, cancel. The billing portal is that screen, ready-made, in two forms that do the same things by the same rules:
| Hosted portal | <BillingPortal /> | |
|---|---|---|
| Where it runs | A page on Base316 (or your custom domain) | Inside your React application |
| Who it is for | Anyone — your app needs no Base316 sign-in | People signed in through Base316 |
| How it is opened | Your server mints a link with a secret key | Rendered under Base316Provider with a session |
| Look | Your hosted sign-in branding | Your app's styles, plus the provider's branding |
| JavaScript | None — forms and redirects only | React |
Both show:
- the current plan and its status, with when it renews or ends;
- the next invoice, line by line, before it is raised;
- a scheduled change — a downgrade at renewal — with a button that undoes it;
- change plan, through a review step: features gained and lost, the prorated credit and charge if it happens now, or the date it waits for, and which of the two is the default (an upgrade now, a downgrade at the renewal);
- seats, when the plan is priced per seat, previewed before anything is charged;
- cancel at the end of the period, asking why and making your retention offer when there is one, and resume until then;
- invoices, with each one's PDF, and a Pay button on an open one;
- the card on file, and a way to replace it;
- a Billing details section showing who invoices are made out to.
Two more sections sit beside the overview, in the portal's navigation:
- Payment methods (
/portal/billing/payment-methods) — every saved card, which one is charged, Make default, Remove, and Add a card (the same Stripe-hosted card entry as "Update card", coming back to this section). Shown unless the account is billed to a parent. See Saved cards. - Credits (
/portal/billing/credits) — each credit balance, buying a pack, and auto-recharge. Shown when the product sells credit packs. See Credit packs.
The hosted portal
Open it from your server
Call this after your own application has signed the person in. The customer id is you vouching for who they are — which is why it needs a secret key, and why the page never takes a customer id from its URL.
import { Base316 } from "@base316/sdk";
const base316 = new Base316({ secretKey: process.env.BASE316_SECRET_KEY });
// In your "Billing" route handler:
const session = await base316.billingPortal.createSession(account.base316CustomerId, {
returnUrl: "https://app.example.com/settings",
});
return redirect(session.url);Or over HTTP:
curl https://api.base316.com/v1/billing-portal/sessions \
-H "Authorization: Bearer $BASE316_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "customerId": "cus_…", "returnUrl": "https://app.example.com/settings" }'To land on a section rather than the overview, pass page:
"overview" (the default), "credits" or "payment-methods". The visit can
move between all of them either way.
const session = await base316.billingPortal.createSession(customerId, {
page: "payment-methods",
returnUrl: "https://app.example.com/settings",
});Every server SDK has the same call: create_billing_portal_session in Python and
Ruby, CreateBillingPortalSession in Go, createBillingPortalSession in Java
and PHP, CreateBillingPortalSessionAsync in C#.
Redirect straight away. The link:
- opens once, and only within ten minutes of being minted. Opening it swaps the token for a cookie and takes the token out of the address bar, so a link left in browser history is already spent;
- then lasts 30 minutes as a visit, long enough to go to Stripe and back;
- opens only the billing portal, for only that customer. A link minted for the API keys page cannot reach billing, and the reverse.
returnUrl is the portal's Back link. It must be one of the environment's
redirect URLs (Authentication → Redirect URLs), matched exactly as written —
the same allow-list the hosted sign-in pages and checkout use. Anything else is
refused with 400 and param: "returnUrl". Leave it out and the page simply
has no back link.
From the dashboard
On a customer's page, Actions → Open billing portal as customer opens the same page in a new tab, and Copy billing portal link copies a fresh link to send them. Both need the Manage permission and are recorded in the audit log, because the link can change and cancel the customer's plan.
Branding
The portal uses the branding you set for your hosted sign-in pages — logo, name, colours, background and support email. On a tenant's own verified domain, it uses that tenant's branding, exactly as the sign-in pages do. See Custom domain and Managed tenants.
How it keeps people safe
- The page has no JavaScript: every action is a form that posts and comes back, so refreshing never repeats a charge, and the CSP forbids scripts.
- Every form carries a CSRF token matched against a cookie only that browser holds; a forged post from another site changes nothing.
- No card number ever reaches your servers or Base316. "Update card" sends the person to a page hosted by Stripe on your connected account, which keeps you in PCI SAQ A. Base316 reads the result back from Stripe and makes it the card future invoices are charged to.
- "Pay" on an open invoice goes to the hosted pay page for that invoice.
- A first purchase, for an account with no plan, goes through checkout — and is
only offered when the session has a
returnUrl, which is where checkout ends. - When the environment sells credit packs, the portal
has a Credits section (and a Manage credits button on the overview
that opens it). To send someone a link that tops up credits and can do
nothing else, mint a credits page link with
creditsPortal.createSession: it opens the same controls on their own, with no way into billing, because a link sent only to top up credits must not be able to cancel the plan.
The React component
import { Base316Provider, BillingPortal, SignIn, useSession } from "@base316/react";
function Billing() {
const { session } = useSession();
if (!session) return <SignIn />;
return <BillingPortal returnUrl="https://app.example.com/settings/billing" />;
}It reads and acts through the signed-in person's session and the /v1/me
routes, so there is nothing to call from your server. Props:
| Prop | What it does |
|---|---|
returnUrl | Where the browser comes back to after updating the card on Stripe's page. A redirect URL. |
featuredPlanKey | The plan to present as the default choice. |
unitLabel | What a seat is called. "seats" by default. |
billingDetails | Your own Billing details section — an address or tax-ID form. Pass null to leave it out. |
It is built from components you can use on their own when you want a different
layout: SubscriptionManager (now with resume, and a confirm step on cancel),
UpcomingInvoice, PricingTable, PlanChangeReview (the review step),
SeatManager, PaymentMethod (the card and its update) and Invoices (now
with PDF download and Pay). See Components.
The /v1/me routes added for it are usable from your own UI too:
POST /v1/me/subscription/resume, GET /v1/me/payment-method,
POST /v1/me/payment-method/update-session and
POST /v1/me/invoices/{id}/pay-link.
One set of rules
The hosted page and the component call the same functions, so they cannot disagree:
- A change now is charged now and needs a card on file; without one the review asks for a card instead. A change at the period end charges nothing and needs no card.
- A scheduled change waits for a pending cancellation to be resumed.
- Adding seats is charged now for the rest of the period; removing them takes effect at the period end and refunds nothing. The customer picks only the number.
- Cancelling is always at the end of the period they have paid for.
A client billed by the firm that runs it
For a managed tenant whose parent pays, both forms
show the parent's plan, read-only, with "Provided by Hartley & Co", and refuse
every change with billed_by_parent — a plan bought on the client's account
would grant nothing, and the firm's plan covers its other clients too. The
upcoming invoice and plan-change preview are not shown, because the firm's
invoice lists every client's usage. The client's own invoices, if it has any,
are listed. There is no card to update: the firm's card is the one charged.
Agreement checkout
Sell a negotiated plan the way procurement buys — review and e-sign an agreement, add a card, activate — and move existing customers across without re-signing.
Sell credit packs
Sell prepaid units of a metered feature — 1,000 AI credits for $10 — through Base316's hosted pay page, show a customer's balance, and hear when it runs low.

