Cancel the team's paid plan (at period end by default)
const url = 'http://localhost:8000/api/billing/subscription/cancel/';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url http://localhost:8000/api/billing/subscription/cancel/ \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{}'Owner-only. Schedules cancellation at period end (Portal-consistent default) or, with immediately=true and platform-admin privilege, cancels right now.
Default (cancel_at_period_end=true)
Stripe flips the sub’s cancel_at_period_end to true and fires customer.subscription.updated. The webhook reflects this locally; the FE shows the pending-cancel banner. At current_period_end, Stripe sends customer.subscription.deleted and _fall_back_to_free mints a Free row with 5 sessions for the team.
Immediate (platform-admin only)
Cancels the Stripe sub now. The fallback fires on the next webhook delivery (customer.subscription.deleted). Used for ops cleanup (test teams, fraud reports). Regular owners sending immediately=true get 400 — protects them from accidentally losing mid-period access on a misclick.
Idempotent
Calling Cancel on a sub that’s already scheduled returns 200 with already_scheduled — the FE can ignore the second click without an error toast.
Authorizations
Section titled “Authorizations”Request Body
Section titled “Request Body”object
When False (default): schedule cancel-at-period-end — the team keeps access through the period they paid for, matches the Stripe Billing Portal config + ADR-0019 §8. When True: cancel the Stripe sub right now. Platform-admin only — regular owners that send true get 400. Owners losing access mid-period is rarely intended; the at-period-end default protects them from a misclick.
Examples
Schedule cancel at period end (default)
{}Immediate cancel (platform admin only)
{ "immediately": true}object
When False (default): schedule cancel-at-period-end — the team keeps access through the period they paid for, matches the Stripe Billing Portal config + ADR-0019 §8. When True: cancel the Stripe sub right now. Platform-admin only — regular owners that send true get 400. Owners losing access mid-period is rarely intended; the at-period-end default protects them from a misclick.
object
When False (default): schedule cancel-at-period-end — the team keeps access through the period they paid for, matches the Stripe Billing Portal config + ADR-0019 §8. When True: cancel the Stripe sub right now. Platform-admin only — regular owners that send true get 400. Owners losing access mid-period is rarely intended; the at-period-end default protects them from a misclick.
Responses
Section titled “Responses”object
Idempotent reply when the sub is already set to cancel at period end. Clicking Cancel twice is safe.
already_scheduled- Cancel was already scheduled
Examples
Already scheduled (idempotent second click)
{ "status": "already_scheduled"}object
scheduled — Stripe accepted the modify call; FE shows the canceled-state banner after the webhook lands. canceled — immediate-cancel path (platform-admin only). In both cases the local row is NOT mutated here; the webhook is authoritative.
scheduled- Cancel scheduled at period endcanceled- Cancel fired immediately (platform admin)
Examples
{ "status": "scheduled"}Canceled immediately
{ "status": "canceled"}immediately_requires_platform_admin (non-admin sent immediately=true), or stripe_cancel_failed.
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_subscription — Free has no in-app cancel (delete the team instead); Strategic is sales-managed off-platform.