Skip to content

Open Stripe Billing Portal pre-filled for a plan change

GET
/api/billing/portal/update-plan/
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.

lang
string

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
ExampleSpanish
es
price
required
string

Stripe 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
ExampleUpgradeToOrganization

Upgrade to Organization

price_1TX64W3IwNiuriuFlSDw2Scy
return_url
string

Where 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).

Media typeapplication/json
object
url
required

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.

string
Examples
ExampleSessionMinted

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).

Media typeapplication/json
object
detail
required

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).

string
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.