Switch the active team for the current admin
const url = 'http://localhost:8000/api/me/active-team/';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"team_id":"0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9"}'};
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/me/active-team/ \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "team_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" }'Flips the signed-in coach’s active_team to another team they belong to. Subsequent requests scope to the new team — clients, sessions, billing, everything tenant-aware. Returns the same shape as GET /api/me/ with the new active context. 404 if the team isn’t one the caller has membership on.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
UUID of a team the caller has a membership on.
Examples
Switch active team
{ "team_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9"}object
UUID of a team the caller has a membership on.
Examplegenerated
team_id=2489E9AD-2EE2-8E00-8EC9-32D5F69181C0object
UUID of a team the caller has a membership on.
Responses
Section titled “Responses”object
Admin UUID.
Firebase Auth user id. Useful for client-side identity mapping.
Admin email — same as the Firebase identity.
Admin display name. Set during onboarding via POST /api/me/onboarding/.
Active team’s name. Reported here as company_name for backward-compat with existing consumers; the field on Team itself is name.
Caller’s role on the active team — owner / member / platform_admin.
True once POST /api/me/onboarding/ was completed. Gates the web shell — false → redirect to /onboarding. Equivalent to next_onboarding_step == 'done'; consumers should prefer next_onboarding_step for fine-grained routing.
True once the admin entered a valid code from the verification email. Platform-admins are auto-stamped at creation. Gates onboarding.
Sessions still available to the active team this billing period. Computed live from the quota ledger; never read from a counter that could drift.
Active subscription snapshot for the active team, or null on free tier.
object
Subscription UUID. Stable across the row’s lifetime (status flips, plan upgrades reuse the same row when Stripe keeps the same sub_… id).
Provider-mirrored lifecycle status. Treat as ‘live’ only when active or trialing — every other value triggers a paywall or banner. Values:
-
active— fully paid, quota usable. -
trialing— Stripe trial in progress, same access asactive. -
past_due— payment failed at least once, Stripe Smart Retries in flight. The FE shows a yellow banner; new sessions 402 withdetail="subscription_suspended". -
unpaid— terminal failure after retries exhausted. The webhook handler will auto-fall-back to Free on the next terminal event. -
canceled— user-initiated cancel that’s already settled (period elapsed). Auto-fallback creates a fresh Free Subscription for the team. -
incomplete— Checkout abandoned before payment confirmation. Stripe garbage-collects after 24h. -
trialing- Trialing -
active- Active -
past_due- Past due -
canceled- Canceled -
incomplete- Incomplete -
unpaid- Unpaid
Frozen snapshot of the plan at subscription time. Even if the underlying Plan changes (sync_plans_from_stripe re-upserts on price/metadata edits), this Subscription row keeps the FK to the original Plan row.
object
Plan UUID. Internal identifier — stable across price + name changes within a tier. For Stripe-touching endpoints (POST /api/billing/checkout/, GET /api/billing/portal/update-plan/), pass stripe_price_id instead.
Stripe price_… id that uniquely identifies this (tier × interval) row in Stripe. This is what POST /api/billing/checkout/ + GET /api/billing/portal/update-plan/?price=<id> consume — pass this value, not the Plan UUID. Free + Strategic carry synthetic non-Stripe ids (bundled_free_month / bundled_strategic_month) because they have no Stripe SKU; the FE should never POST those to the Stripe-touching endpoints (filter on role !== 'free' && role !== 'strategic' client-side, mirroring how the BE views 400 those targets).
Display name shown to the buyer (Free, Starter, Professional, Organization, Strategic).
Billing cadence — month or year. Free + Strategic are monthly-only (Decision H + I); yearly variants exist only for Starter / Pro / Org.
month- Monthlyyear- Yearly
Price in minor units (cents/kopecks). Divide by 100 for the headline price. 0 for Free and Strategic — for Strategic the actual contract is sales-managed off-platform and display_mode=custom tells the FE not to render the price block.
ISO 4217 currency code (lowercase, e.g. usd). v1 ships USD-only; multi-currency is a v1.6 follow-up.
Canonical tier identifier: free / starter / professional / organization / strategic. Use name for display and role for tier-based feature gating server-side. Stable across price + name changes within a tier.
How the FE should render this row. default shows price + quota + ceilings (the four paid tiers + Free). custom skips those fields and shows the sales-contact CTA (Strategic). Branching key for the pricing grid and SubscriptionPanel — see ADR-0019 §2 + §3.
default- Defaultcustom- Custom
Sessions per billing period. Quota refreshes to exactly this number each renewal — leftover does NOT roll over (Decision 6). 0 for Strategic (engagement-specific, off-platform).
Team-membership ceiling. Inviting past this returns 402 with detail="seat_limit_reached". Free=1, Starter=1, Pro=3, Org=10, Strategic=0 (gate skipped for display_mode=custom).
Client-row ceiling. Creating past this returns 402 with detail="client_limit_reached". Free=1, Starter=50, Pro=250, Org=1000, Strategic=0 (gate skipped for display_mode=custom).
Reserved for downstream pricing differentiation (segment-aware plans, ADR-0019 Decision J). Ships inert in v1.5 — PlanListView does NOT filter by team segment, so the field is informational only. Empty list = applies to all segments. v1.6 will activate the filter without an additional migration.
ISO-8601 UTC. Stamped on every renewal — the FE uses this to render ‘Renews every N days’ copy.
ISO-8601 UTC when the current period (and the included quota) ends. On paid tiers this is when Stripe will next charge; on Free this is when the monthly renew_free_quota cron refreshes the 5-session balance.
ISO-8601 UTC when the Stripe trial flips to billed. Null when the subscription wasn’t created with a trial. Display a ‘Trial ends on …’ banner when set and status == "trialing".
True when the buyer scheduled cancellation via the in-app Cancel button (Stripe modify with cancel_at_period_end=True). The buyer keeps access until current_period_end. Stripe Billing Portal-driven cancels may use the sibling cancel_at field instead — prefer the is_scheduled_to_cancel-equivalent FE predicate that ORs both.
ISO-8601 UTC of an explicit cancel timestamp set by Stripe (typically via Billing Portal’s ‘cancel on a date’ config, or schedule-driven cancels). When set, the FE should render the ‘ACCESS ENDS’ pill even if cancel_at_period_end is False. Null when no explicit timestamp is set.
ISO-8601 UTC when Stripe last recorded a cancellation action against this subscription. Not the moment access ends — that’s current_period_end or cancel_at. Read for audit / display only.
Stripe’s cancellation_details.reason enum value (cancellation_requested, payment_disputed, payment_failed, canceled_by_retention_policy) or empty string when absent. Surfaced for support / analytics — not used in any gate.
True for non-Stripe rows (Free signup default, Strategic sales-managed). When True: stripe_subscription_id + stripe_customer_id are null, the Stripe Billing Portal endpoint 404s, and the renewal clock is driven by our renew_free_quota cron (Free) or sales (Strategic). False for every Stripe-managed paid row.
Unified predicate — True iff the sub has scheduled cancellation but still grants paid access. Mirrors the model @property at domain/models.py::Subscription.is_scheduled_to_cancel, which ORs the three Stripe-side cancel paths (cancel_at_period_end / cancel_at timestamp / schedule-driven canceled-while-period-active) and returns False once status flips to canceled / unpaid. The FE should consume this single field rather than re-deriving the OR locally — keeps the predicate in lockstep with BE gates (the in-app POST /api/billing/subscription/cancel/ + POST /api/billing/subscription/reactivate/ endpoints read it; plan-change goes through the Portal deep-link GET /api/billing/portal/update-plan/, where Stripe hides the upgrade button when the sub is scheduled to cancel).
Where the row came from — the single source-of-truth kind. stripe (self-serve paid; the only value with is_managed_externally=False), free (registration default), strategic (sales-managed), legacy_migration (migration bridge), or admin_grant (complimentary tier granted from the admin). The FE branches on this: any non-stripe value has no Billing Portal and routes plan changes through Checkout, and admin_grant renders a comp badge with the real tier name rather than ‘Free’.
stripe- Stripe (self-serve paid)free- Free (registration default)strategic- Strategic (sales-managed)legacy_migration- Legacy migration bridgeadmin_grant- Admin-granted (comp / trial)
Every team this admin has a membership on, including their role and which one is currently active. Lets the shell render a switcher without a second round-trip.
object
Team UUID.
Team display name.
Caller’s role on this team — owner or member.
True for the team that everything tenant-scoped currently resolves through (role, session_quota, subscription on Me reflect this team).
Email of the team owner. Empty when not resolvable (rare — only during transient cleanup states).
Active team’s market segment — one of sports, healthcare, wellness, or empty string. Set at onboarding step 1 by the team owner; reserved for future pricing differentiation. Empty for legacy teams or teams whose owner hasn’t completed step 1 yet — those owners get routed back via next_onboarding_step == 'segment'. Modelled as string (not a typed enum) on read so the empty case is one literal, not a SegmentEnum | BlankEnum union; write-side still validates against Segment.choices.
Activities the active team trains, picked at onboarding step 2 by the team owner alongside segment. Both describe the team’s identity, so invitees inherit them and skip the step. Empty list = the team’s owner hasn’t completed step 2 yet → onboarding gate routes back via next_onboarding_step == 'activities'. Each element is an Activity value.
Active team’s white-label branding for shared / exported Solution reports (logo, accent colour, footer text, brand name, contact email). Owner-edited via PATCH /api/me/branding/; the logo is uploaded via POST /api/me/branding/logo/.
object
Display name on the report lockup, as stored. Empty when the team never set one — the report then self-attributes to the running product’s brand. The workspace name is never used as a fallback (#1979).
Logo image URL (uploaded asset in the media bucket, or a CDN URL). Empty when unset.
Hex accent colour (#rrggbb / #rgb) for report CTAs. Empty when unset.
Short attribution shown in the report footer. Empty when unset.
Optional contact email rendered as a footer mailto:. Empty when unset.
Timestamp of when the admin opened a demo session at onboarding step 5. Null = step not completed.
segment- Pick segmentactivities- Pick activitiesdemo- View demoaccount- Account + teamdone- Done
Computed gate the SPA’s onboarding guard reads to decide the next funnel route. Cascade: segment → activities → demo → account → done. The first unsatisfied step wins; once all are satisfied the value is done and the shell unlocks. null when the admin hasn’t yet verified their email — auth lives outside the onboarding funnel (per design), so clients route on email_verified first and only consult this field once verification is done. Single source of truth for the onboarding redirect inside the funnel; clients should not re-derive funnel routing from the individual segment / preferred_activities / demo_completed_at fields.
segment- Pick segmentactivities- Pick activitiesdemo- View demoaccount- Account + teamdone- Done
True when server-persisted Kai threads (#945) are enabled on this environment. Gates the SPA’s threads drawer: false → the legacy stateless support chat. Environment-level (an env flag), not per-account — surfaced here so the SPA needs no extra round-trip and no rebuild when the flag flips.
Per-admin notification consent (#1868): whether the Capture Check email — “this session needs a re-record” — may be sent to this admin when a completed session they uploaded carries advisory capture findings. Defaults to true (a service message about their own upload); flipped in Settings → Notifications or via the email’s no-login unsubscribe link. Delivery is additionally gated by the deployment flag and a 24h per-uploader cadence, so true does not by itself mean emails flow.
Per-admin privacy default (#370): the upload wizard’s per-session Face Blur toggle starts from this value (#1038), so when true every upload defaults to anonymising the subject’s face in the analysed (annotated) video. The coach can still flip blur for a single session without changing this default; API callers set blur_face per session on POST /api/sessions/. Off by default; set via PATCH /api/me/.
Ids of one-time UI hints (nudges) this admin has permanently dismissed via POST /api/me/hints/dismiss/ (#1038). The frontend gates a ‘don’t ask again’ nudge on membership here, so a dismissal holds across every device the coach signs in on. Free-form, frontend-owned ids (e.g. upload_blur_save_prompt).
Example
{ "subscription": { "status": "trialing", "plan": { "interval": "month", "display_mode": "default" }, "managed_source": "stripe" }, "next_onboarding_step": "segment", "support_threads": false}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"}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"}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).
Examples
404 — Not a team you belong to
{ "detail": "team_not_found"}