Skip to content

Server-sent events stream of AI coaching tokens

GET
/api/sessions/{id}/coaching/
curl --request GET \
--url http://localhost:8000/api/sessions/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/coaching/ \
--header 'Authorization: Bearer <token>'

Returns an SSE stream of LLM-generated coaching tokens for the session, written in plain language for the athlete. Frame format: event: meta (one-time prelude with session/lang/system metadata), then a sequence of event: token frames each carrying {text}, then event: done {ok: true}. Optional query params: focus to bias the analysis at one metric/area, lang (en / ru) to switch the LLM’s output language, units (metric / imperial) to control km/h vs mph + cm vs in. The completed text is cached on the session and reused by the PDF endpoint. A single event: error frame with HTTP 404 is sent for an unknown session; 402 if the team’s coaching quota is exhausted.

id
required
string format: uuid
focus
string

Optional metric/area to bias the analysis toward.

lang
string

en (default) or ru.

units
string

metric (default) or imperial.

A text/event-stream (SSE) response. Frames arrive as event: <name>\ndata: <json>\n\n: one meta prelude, a sequence of token frames each carrying {text}, then a final done {ok: true}. On error a single error frame {detail} is sent and the HTTP status is set to 404 (unknown session/token).

Media typeapplication/json
string
Examples
ExampleSSEFrameSequence(illustrative)

SSE frame sequence (illustrative)

Server-sent events; the actual response is text/event-stream. The example shows the concatenated frames as they arrive — meta first, a sequence of token, done last.

event: meta
data: {"session_id":"5c836d7d","activity":"running","client":"Anna Petrova","focus":null,"lang":"en","system":"metric","model":"","provider":"","mode":"live"}
event: token
data: {"text":"Cadence "}
event: token
data: {"text":"of "}
event: token
data: {"text":"169 spm "}
event: token
data: {"text":"is on the low side; "}
event: token
data: {"text":"target 175-180 for this pace.\n"}
event: done
data: {"ok":true}
Media typeapplication/json
object
detail
required

One of five canonical strings. The front-end paywall switches copy + CTA on this value (exact string equality — do not localise). quota_exhausted / seat_limit_reached / client_limit_reached / feature_not_in_plan lead to an upgrade CTA; subscription_suspended leads to a Billing Portal CTA so the buyer can update their payment method.

  • quota_exhausted - Session quota for the period reached 0
  • subscription_suspended - Stripe dunning in flight (past_due / unpaid)
  • seat_limit_reached - Adding another team member would exceed plan.max_seats
  • client_limit_reached - Adding another Client would exceed plan.max_client_profiles
  • feature_not_in_plan - The tier does not include this whole feature surface
string
Allowed values: quota_exhausted subscription_suspended seat_limit_reached client_limit_reached feature_not_in_plan
reset_at
required

ISO-8601 UTC timestamp the team’s current period ends. Null when the team has no live subscription (legacy un-migrated row). On Free + paid this is the renewal moment; quota refreshes to exactly plan.session_quota (NOT additive — leftover sessions don’t roll over).

string format: date-time
nullable
current_plan
required

The active plan’s role string (free, starter, professional, organization, strategic). Same value as current_role — kept as two separate fields for forward-compatibility with a future named-plan split where current_plan could carry an SKU and current_role the tier label.

string
current_role
required

The active plan’s role string. See current_plan.

string
feature

Which feature surface is locked. Present ONLY on feature_not_in_plan, so one modal can name what’s gated without a detail string per feature (#773). The only value today is agents (aikynetix/agents/services.py::AGENTS_FEATURE).

string
Example
{
"detail": "quota_exhausted"
}
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"
}