Open Stripe Billing Portal pre-filled for a plan change
const url = 'http://localhost:8000/api/billing/portal/update-plan/?lang=es&price=price_1TX64W3IwNiuriuFlSDw2Scy';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url 'http://localhost:8000/api/billing/portal/update-plan/?lang=es&price=price_1TX64W3IwNiuriuFlSDw2Scy' \ --header 'Authorization: Bearer <token>'Mints a one-shot Stripe Billing Portal session with flow_data.type = subscription_update_confirm so the buyer lands directly on the proration-preview / ‘Confirm and pay’ screen for the target Stripe price. Stripe owns the proration math + the confirm screen; the SPA just redirects the browser to the returned url.
Owner-only. Returns 404 when the team has no paid Stripe subscription (Free / Strategic teams hit this — they need CheckoutSessionView, not this endpoint). Returns 400 on validation failures (unknown_price, invalid_target_plan, already_on_plan, subscription_item_unavailable).
Stripe Portal config requirement
The Portal config must enable the subscription_update feature with proration_behavior = create_prorations and a products list that includes every paid Plan tier. Without it the session-create call 4xx’s with a vague body. Documented in ops/runbooks/billing-stripe-setup.md.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Query Parameters
Section titled “Query Parameters”FE language code (en / es / ru — mirrors apps/web SUPPORTED_LANGS). Mapped to Stripe locale so the Portal page renders in the language the buyer picked in Settings → Language. Unknown / unset → auto (Stripe’s documented fallback). Append here when the SPA picker grows.
Examples
esStripe price id (price_…) of the target plan. Must reference an existing paid Plan row (display_mode != 'custom') that differs from the team’s current plan.
Examples
Upgrade to Organization
price_1TX64W3IwNiuriuFlSDw2ScyWhere Stripe sends the buyer when they close the Portal. Validated against settings.app_base_url; third-party URLs are rejected silently and the default /settings?section=subscription is substituted (open-redirect guard, mirrors BillingPortalView).
Responses
Section titled “Responses”object
Hosted Stripe Billing Portal URL pinned to the proration-preview / Confirm-and-pay screen for the target price. Single-use; mint a fresh one per click.
Examples
Session minted
{ "url": "https://billing.stripe.com/p/session/test_YWNjdF8xUVhZWlozNTY_b2c3d4e5f6"}Validation failure. detail is one of price_required (no ?price= query param), unknown_price (price not in the catalog), invalid_target_plan (Strategic target — no Stripe SKU), already_on_plan (target equals current), subscription_item_unavailable (paid sub predates migration 0020 and has no Stripe event since — the FE should toast and route to the manage-billing link), or stripe_portal_unavailable (Stripe SDK error).
object
Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).
Examplegenerated
{ "detail": "example"}Caller is not the team owner. Also returned to a partner aik_… API key: billing writes require the Firebase scheme (billing is read-only for keys).
No paid Stripe subscription for the active team. Free / Strategic teams hit this — they need CheckoutSessionView, not this endpoint.