Skip to content

Dismiss a one-time UI hint ('don't ask again')

POST
/api/me/hints/dismiss/
curl --request POST \
--url http://localhost:8000/api/me/hints/dismiss/ \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "hint_id": "upload_blur_save_prompt" }'

Permanently dismisses one one-time UI nudge for the signed-in coach (#1038). hint_id is a free-form, frontend-owned slug (e.g. upload_blur_save_prompt) — the server is a generic dismissed-set store and deliberately does not enumerate hints, so a new nudge is a frontend-only string. Account-scoped (stored on Admin.preferences, NOT device-local storage) so the dismissal holds on every device the coach signs in on. Idempotent — re-dismissing is a no-op. Returns the full profile (same shape as GET /api/me/) so the client refreshes dismissed_hints from the response. Per-admin — no owner gate.

POST /api/me/hints/dismiss/ request (#1038) — permanently dismiss one one-time UI hint for the signed-in admin.

hint_id is a free-form, frontend-owned slug (e.g. upload_blur_save_prompt); the server is a generic dismissed-set store and deliberately does not enumerate hints, so a new nudge is a frontend-only string (no server enum to churn). Idempotent — dismissing an already-dismissed hint is a no-op.

object
hint_id
required

Hint id to dismiss — a lowercase slug ([a-z0-9_-], ≤ 64 chars, alphanumeric start). Frontend-owned; e.g. upload_blur_save_prompt.

string
>= 1 characters <= 64 characters /^[a-z0-9][a-z0-9_-]*$/

Examples

ExampleDismissTheUploadBlurSave-prompt

Dismiss the upload blur save-prompt

{
"hint_id": "upload_blur_save_prompt"
}
Media typeapplication/json
object
id
required

Admin UUID.

string format: uuid
firebase_uid
required

Firebase Auth user id. Useful for client-side identity mapping.

string
email
required

Admin email — same as the Firebase identity.

string
display_name
required

Admin display name. Set during onboarding via POST /api/me/onboarding/.

string
company_name
required

Active team’s name. Reported here as company_name for backward-compat with existing consumers; the field on Team itself is name.

string
role
required

Caller’s role on the active team — owner / member / platform_admin.

string
onboarded
required

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.

boolean
email_verified
required

True once the admin entered a valid code from the verification email. Platform-admins are auto-stamped at creation. Gates onboarding.

boolean
is_ambassador
required

True when an ambassador record (#2253) points at this account: the shell shows the Partner tab and /partner opens. Everything the page shows comes from GET /api/partner/me/.

boolean
session_quota
required

Sessions still available to the active team this billing period. Computed live from the quota ledger; never read from a counter that could drift.

integer
granted_quota
required

The active team’s session ceiling this period: max(allotment, session_quota + sessions spent since current_period_start), where the allotment is plan.session_quota, or the operator’s count on an admin grant (#2578). Holds still as the balance drops, so a team credited above its plan reads 6 of 30, not 6 of 6. session_quota never exceeds it. 0 = no ceiling (no subscription, or a plan with no quota such as Strategic) — show the bare balance.

integer
quota_allotment
required

What one renewal refills the balance to: plan.session_quota, or the operator’s count on an admin grant (#2578). Compare a ceiling against this, not subscription.plan.session_quota, to tell a one-off credit from the period’s allotment. 0 = no ceiling.

integer
subscription
required

Active subscription snapshot for the active team, or null on free tier.

object
id
required

Subscription UUID. Stable across the row’s lifetime (status flips, plan upgrades reuse the same row when Stripe keeps the same sub_… id).

string format: uuid
status
required

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 — a trial in progress, same access as active: a Stripe trial, or the self-serve no-card trial (managed_source=trial, ADR-0053).

  • past_due — payment failed at least once, Stripe Smart Retries in flight. The FE shows a yellow banner; new sessions 402 with detail="subscription_suspended".

  • unpaid — terminal failure after retries exhausted. The team goes read-only on the next terminal event (or falls back to Free where the self-serve trial is off).

  • canceled — user-initiated cancel that’s already settled (period elapsed). The team goes read-only (/api/me/ access.reason = access_ended) — or, where the self-serve trial is off, a fresh Free Subscription is created. An ended self-serve trial is also canceled.

  • 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

string
Allowed values: trialing active past_due canceled incomplete unpaid
plan
required

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
id
required

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.

string format: uuid
stripe_price_id
required

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

string
<= 128 characters
name
required

Display name shown to the buyer (Free, Starter, Professional, Organization, Strategic).

string
<= 100 characters
interval
required

Billing cadence — month or year. Free + Strategic are monthly-only (Decision H + I); yearly variants exist only for Starter / Pro / Org.

  • month - Monthly
  • year - Yearly
string
Allowed values: month year
amount_cents
required

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.

integer
<= 2147483647
currency

ISO 4217 currency code (lowercase, e.g. usd). v1 ships USD-only; multi-currency is a v1.6 follow-up.

string
<= 3 characters
role
required

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.

string
<= 50 characters
display_mode

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 - Default
  • custom - Custom
string
Allowed values: default custom
session_quota

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

integer
<= 2147483647
max_seats

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

integer
<= 2147483647
max_client_profiles

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

integer
<= 2147483647
allowed_segments

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.

current_period_start

ISO-8601 UTC. Stamped on every renewal — the FE uses this to render ‘Renews every N days’ copy.

string format: date-time
nullable
current_period_end

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 balance to the plan’s quota; on a self-serve trial it equals trial_end.

string format: date-time
nullable
trial_end

ISO-8601 UTC when the trial ends: a Stripe trial flips to billed; a self-serve trial (managed_source=trial) turns the team read-only. Null when the subscription wasn’t created with a trial. Display a ‘Trial ends on …’ banner when set and status == "trialing".

string format: date-time
nullable
cancel_at_period_end

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.

boolean
cancel_at

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.

string format: date-time
nullable
canceled_at

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.

string format: date-time
nullable
cancellation_reason

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.

string
<= 64 characters
is_managed_externally

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.

boolean
is_scheduled_to_cancel
required

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

boolean
managed_source

Where the row came from — the single source-of-truth kind. stripe (self-serve paid; the only value with is_managed_externally=False), trial (the self-serve no-card trial — registration default where it is on, ADR-0053), free (registration default where it is off), 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 bridge
  • admin_grant - Admin-granted (comp / trial)
  • trial - Self-serve trial (registration default, ADR-0053)
string
Allowed values: stripe free strategic legacy_migration admin_grant trial
access
required

Whether the active team is read-only (#2901, ADR-0053) — the same answer the write endpoints’ 402 gives.

object
read_only
required

True when the team can open everything it has but create nothing new — no session, upload, client, invite, share link or Solutions run. Every such endpoint answers 402 with reason as its detail.

boolean
reason
required
One of:
  • trial_expired - trial_expired
  • access_ended - access_ended
string
Allowed values: trial_expired access_ended
trial_end
required

When the team’s self-serve trial ends (or ended). Null for a team that was never on one. During a live trial the countdown reads this plus session_quota.

string format: date-time
nullable
teams
required

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.

Array<object>
object
id
required

Team UUID.

string format: uuid
name
required

Team display name.

string
role
required

Caller’s role on this team — owner or member.

string
is_active
required

True for the team that everything tenant-scoped currently resolves through (role, session_quota, subscription on Me reflect this team).

boolean
owner_email
required

Email of the team owner. Empty when not resolvable (rare — only during transient cleanup states).

string
segment
required

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.

string
preferred_activities
required

Activities the active team trains. An explicitly saved list narrows the segment default and is inherited by invitees. Empty list = no custom selection; ClientsPage falls back to the segment’s activities instead of routing back to an onboarding picker. Each element is an Activity value.

Array<string>
branding
required

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
brand_name
required

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

string
logo_url
required

Logo image URL (uploaded asset in the media bucket, or a CDN URL). Empty when unset.

string
accent_color
required

Hex accent colour (#rrggbb / #rgb) for report CTAs. Empty when unset.

string
footer_text
required

Short attribution shown in the report footer. Empty when unset.

string
contact_email
required

Optional contact email rendered as a footer mailto:. Empty when unset.

string
qr_url
required

Link encoded as a QR code at the end of every report (PDF exports and the Solutions web report / share link). Empty when unset — then no QR is drawn anywhere (#2227).

string
qr_caption
required

One line under the QR saying what it is for. Rendered only together with qr_url. Empty when unset.

string
qr_data_url
required

Read-only, derived: the QR for qr_url as an <img>-ready data:image/svg+xml;base64,… string — the same image every report draws. Empty when qr_url is unset. Not writable; set qr_url instead.

string
privacy
required

Active team’s data retention (#2752): automatic session deletion and deleting unblurred originals. Owner-edited via PATCH /api/privacy/team/; can_edit is true for the owner only.

object
delete_unblurred_originals
required

Delete the unblurred upload once a face-blurred analysis has rendered its blurred copies and unblurred_original_grace_hours have passed. Such a session can no longer be re-analysed.

boolean
unblurred_original_grace_hours
required

How long a completed blurred session keeps its original, for a re-cut.

integer
agent_thread_retention_days
required

Agent conversations: each turn and intake is deleted this many days after it was written. 0 = kept.

integer
support_thread_retention_days
required

Kai support conversations: each turn and file is deleted this many days after it was written. 0 = kept.

integer
data_export_ttl_days
required

How long a finished export’s archive can be downloaded before it is deleted.

integer
can_edit
required

True for the team’s owner, the only role that may change these.

boolean
demo_completed_at
required

Timestamp of when the admin opened a demo session at onboarding step 5. Null = step not completed.

string format: date-time
nullable
next_onboarding_step
required
One of:
  • terms - Accept the Terms
  • segment - Pick segment
  • activities - Pick activities
  • demo - View demo
  • account - Account + team
  • done - Done
string
Allowed values: terms segment activities demo account done
client_documents

True when documents on the athlete (#2693) are enabled on this environment: the client page shows its Documents section and /api/clients/<id>/documents/ answers. Environment-level (an env flag), not per-account; off by default everywhere.

boolean
client_problem_notes

True when follow-up notes on an athlete’s problems (#2693) are enabled on this environment: the client page’s injury timeline shows the notes and /api/clients/<id>/problem-notes/ answers. Environment-level (an env flag), not per-account; off by default everywhere.

boolean
support_threads

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.

boolean
client_access

True when client access and the coach-client chat (#2503) are enabled on this environment. Gates the coach-side App access block, the access chips on the client list, the chat tab and the inbox. 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.

boolean
portal_card_count

How many client cards (#2503) this account holds active access to: an athlete’s or a guardian’s view of the results a coach shared. With no team and at least one card the app opens the client portal; a coach with one gets a ‘My results’ entry.

integer
0
notify_capture_check_email
required

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.

boolean
notify_chat_message_email
required

Per-account consent (#2503): whether a coach ↔ client message left unread for 10 minutes e-mails this person — at most one per conversation every 6 hours, never with the words. Defaults to true; the e-mail’s unsubscribe link flips it off.

boolean
notify_client_upload_email
required

Per-account consent (#2503): whether a clip a client sent from the portal e-mails this coach (the same unread rule). Defaults to true; the e-mail’s unsubscribe link flips it off.

boolean
blur_face_by_default
required

Per-admin privacy default (#370): when true, this coach’s uploads anonymise the subject’s face in the analysed (annotated) video. Every upload path follows it — a POST /api/sessions/ without blur_face takes this value, and the upload wizard’s per-session Face Blur toggle starts from it (#1038), so the coach can flip blur for a single session without changing this default. On by default (#2577): true unless the coach saved false; set via PATCH /api/me/.

boolean
dismissed_hints
required

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

Array<string>

Examples

ExampleSigned-inCoachOnFree

Signed-in coach on Free

{
"id": "f6d52e64-3aa3-4a26-bac2-0fc9d718448d",
"firebase_uid": "9xKqz…",
"email": "coach@club.example",
"display_name": "Head Coach",
"company_name": "Riverside Track Club",
"role": "owner",
"onboarded": true,
"email_verified": true,
"session_quota": 2,
"granted_quota": 0,
"subscription": null,
"teams": [
{
"id": "2648a781-8751-47c0-836d-2c0189792d71",
"name": "Riverside Track Club",
"role": "owner",
"is_active": true,
"owner_email": "coach@club.example"
}
],
"segment": "sports",
"preferred_activities": [
"running",
"jumps"
],
"branding": {
"brand_name": "Riverside Track Club",
"logo_url": "https://cdn.example/riverside-logo.svg",
"accent_color": "#1f6feb",
"footer_text": "Riverside Track Club · riverside.example",
"contact_email": "coach@club.example"
},
"demo_completed_at": "2026-04-20T10:00:00Z",
"next_onboarding_step": "done"
}
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

Example generated

{
"detail": "example"
}
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

Example generated

{
"detail": "example"
}