Skip to content

Coach review for a session

POST
/api/sessions/{id}/review/
curl --request POST \
--url http://localhost:8000/api/sessions/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/review/ \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "notes": "example", "score": 1 }'

Persists the coach’s free-text notes (and optional 0..100 score) for a session. Stored on the Session via a coach_review SessionMetric so the review travels with every export (PDF, share link, list views) and ends up in the roster feed alongside the original analysis. Idempotent — calling twice updates the previous review rather than appending. Returns the updated Session.

409 session_not_reviewable while the session is still pending / processing — a coach can only review a completed or failed session (a failed one is reviewable so they can note ‘bad footage, re-shoot’).

id
required
string format: uuid

Request body for POST /api/sessions/<id>/review/.

object
notes
required

Coach’s free-text review. Stored on the session and shown in every export.

string
<= 4000 characters
score

Optional coach score, 0..100. Null leaves the analysis score untouched.

number format: double
nullable
Examplegenerated
{
"notes": "example",
"score": 1
}
Media typeapplication/json
object
id
required

Server-assigned UUID. Use the same value as session_id from the upload-intent step to keep the storage object key consistent.

string format: uuid
activity
required

Activity family — running, walking, jumps, weightlifting, agility, mobility, fencing, ergonomics. Drives which analyzer runs.

  • running - Running
  • walking - Walking
  • jump - Vertical Jump
  • weightlifting - Weightlifting
  • mobility - Mobility Assessment
  • workspace_wellness - Workspace Wellness
  • agility - Agility
  • fencing - Fencing
  • cycling - Cycling
  • tennis - Tennis
  • padel - Padel Tennis
  • bowling - Bowling
  • baseball - Baseball
  • football - Football
  • badminton - Badminton
  • cricket - Cricket
  • basketball - Basketball
  • scuba_diving - Scuba Diving
  • ice_hockey - Ice Hockey
  • american_football - American Football
  • fri - Falls Risk Indicator
string
Allowed values: running walking jump weightlifting mobility workspace_wellness agility fencing cycling tennis padel bowling baseball football badminton cricket basketball scuba_diving ice_hockey american_football fri
exercise

Specific movement within the activity (e.g. Treadmill, Back Squat, Single-Leg Stand). Pick from GET /api/exercises/?activity=....

string
<= 80 characters
name
required
string
seq_no
required

Stable per-team session number (#941), assigned at create time and immutable — the shared reference coaches use to point at a video (‘look at session 42’). Monotonic per team in creation order; survives filter / sort / pagination and reads the same for every teammate. Gaps appear where sessions were deleted (numbers are never reused). null on demo sessions (cross-team, deliberately unnumbered) and on the public /api/shared/{token}/ payload, where the team-internal ordinal is withheld from anonymous viewers.

integer
nullable
status
required

Lifecycle state: pending (queued) → processing (analyzer running) → completed (metrics + annotated mp4 ready) | failed (see failure_reason).

  • pending - Pending
  • processing - Processing
  • completed - Completed
  • failed - Failed
string
Allowed values: pending processing completed failed
video_url
Any of:
string format: uri
<= 1000 characters
analyzed_url
required

Public URL of the annotated playback mp4 — pose skeleton drawn over the original frames. Populated when status == 'completed'; empty before that and during reanalyze.

string format: uri
anonymized_url
required

Public URL of a face-anonymised copy of the source video WITHOUT the pose overlay (#1233) — the privacy-safe ‘raw’ source for the player’s overlay toggle. Populated only on Face-Blur sessions (blur_face == true); empty otherwise, when the best-effort clean render failed, and during reanalyze. On non-blur sessions the player toggles to video_url directly. On the public /api/shared/{token}/ payload the un-anonymised video_url is withheld for blur sessions and this field is the only ‘raw’ source.

string format: uri
pose3d_url
required

Public URL of the 3D pose sidecar JSON (#1356) — the per-frame 3D joint track uploaded next to the annotated mp4, consumed by the player’s interactive 3D viewer. Empty when the 3D lifter isn’t configured on the inference service, when its best-effort generation failed, and during reanalyze — clients then hide the 3D view. Served on the public /api/shared/{token}/ payload, EXCEPT on a Face-Blur session: it carries no imagery, but it reconstructs the athlete’s movement in full, so it is blanked there alongside video_url.

string format: uri
thumbnail_url
required

Public URL of a static JPG poster (~5-15 KB) extracted at ~0.5 s of the annotated mp4. The SPA renders this on the journal row instead of mounting a <video preload=metadata> per row, which avoids the main-thread freeze on long session lists. Empty when aikmodels couldn’t generate one — clients fall back to analyzed_url and decode the first frame themselves.

string format: uri
fps

Frame rate the session was ANALYSED at — read off the normalized file, not the upload. Every metric and per-frame derivative in the report is a function of this number, which is why it is the one reported. It is min(AIKMODELS_NORMALIZE_FPS, source_fps) (#1951), so on a 60 or 120 fps upload it reads BELOW what the coach filmed; compare source_fps to tell the two apart.

integer
nullable <= 32767
source_fps

Frame rate the camera actually captured at, probed before our normalize resample. Null on older sessions and when the probe could not run. Present so a client can say “filmed at 60, analysed at 30” rather than showing our rate as though it were the coach’s (#1951). May be fractional — 23.976 and 29.97 are real capture rates.

number format: double
nullable
duration_ms

Source video duration in milliseconds.

integer
nullable <= 2147483647
processed_at
required

ISO-8601 UTC timestamp of the session’s LAST successful analysis. Null until the first one completes; it is NOT cleared afterwards, so a session that delivered a report once and was then re-analyzed keeps its timestamp while sitting on pending/processing/failed.

string format: date-time
nullable
created_at
required

ISO-8601 UTC timestamp of session creation.

string format: date-time
client
required

UUID of the Client (athlete) the session is about. Optional but strongly recommended — without a client, body-aware metric adjustments fall back to defaults.

string format: uuid
nullable
uploaded_by
required

UUID of the Admin who uploaded the session. Team-scoped listings carry it so per-coach surfaces (e.g. the upload wizard’s recent-combos row) can filter to the current admin’s own history without leaking a teammate’s picks. null for legacy rows created before attribution was recorded.

string format: uuid
nullable
failure_reason
required

Human-readable explanation when status == 'failed'. Empty otherwise.

string
metrics
required

List of all SessionMetric rows attached to this session — joint angles, timings, scores, the full per-key catalogue the analyzer emitted. Empty until status == 'completed'.

Array<object>
object
key
string
value_num
number
nullable
value_json
object
metric_targets
required

Body-aware-adjusted green-zone bounds keyed by metric: {key: {min_good, max_good, amber_low, amber_high, higher_is_better, beta}}. Already accounts for the athlete’s gender + height — front-end can render the zone directly without re-applying any adjustment. amber_low / amber_high are the ONROM (amber) outer bounds of the three-tier clinical band (#521); both null ⇒ classic binary green zone (the front-end derives the wnl/onrom/high_risk tier from them). beta flags a metric that has not cleared clinical validation (UI shows a precision caveat). Empty list when the activity has no zone-defining specs.

object
key
additional properties
camera_view
required

side / front / back. Read-only; derived from the value passed at session create time. Lets the player UI badge the view and hide metric cards that don’t apply.

string
capture_findings
required

Capture-quality findings for a COMPLETED session (#1868) — the typed defect taxonomy evaluated from the inference service’s capture signals: [{code, severity, data}], e.g. {"code": "pose_sparse", "severity": "advisory", "data": {"detection_ratio": 0.42}}. Empty list = evaluated clean; null = the session predates capture evaluation (never conflate the two). Codes: pose_sparse / implausible_geometry / subject_too_far / camera_off_plane / subject_left_frame. Severity is advisory-only today — fatal capture problems FAIL the session and speak through failure_reason.

Array<object>
nullable
object
key
additional properties
handedness
required

right / left dominant hand or foot, or null. Read-only; from the value passed at create time. Seeds the reprocess dialog so a handedness-aware re-run keeps the athlete’s side instead of silently defaulting to right-handed (#775).

string
nullable
is_demo
required
boolean
progress
required

Worker’s most recent stage emit while this session is in flight. null for terminal states, queued sessions, and any read failure — the SPA renders a queued placeholder in those cases. Polled via the existing session-detail refetch (every 2 s while pending/processing); no separate endpoint.

object
stage
required

Pipeline stage the worker is currently executing (e.g. download, pose_extract, draw). The full stage list is hardcoded on each side and kept in sync via a contract test (apps/api side) — adding or renaming a stage is a cross-track PR per ADR-0011.

string
stage_idx
required

0-based index of stage in the worker’s stage order.

integer
frame_done
required

Frames processed so far inside the current stage. Only moves during stages with sub-progress (today: pose_extract); 0 elsewhere.

integer
frame_total
required

Total frame count for the current stage; 0 when the stage has no frame counter or the worker hasn’t computed it yet.

integer
has_active_share
required

Whether a live public share link exists for this session — so the player can offer Revoke on a session shared in an earlier visit. null means NOT REPORTED, which is every list response: the value is annotated by the session-detail view only, because a per-row lookup would be one query per session on a list that has no use for it. Treat null as ‘unknown’, not as ‘no share’.

boolean
nullable
locale
required

Language (en / ru / es) frozen on the share when the coach created/refreshed it — only populated on the public GET /api/shared/{token}/ response, where the /r/<token> page reads it to render its whole UI in the same language as the AI coaching paragraph (issue #339). null on the authenticated session endpoints, which follow the coach’s own UI locale.

string
nullable
blur_face
required

True when this session was created with the Blur Face option — the analysed video has the subject’s face anonymised. Read-only; false for every session created without the flag.

boolean
barbell_mass_kg
required

The lifter’s declared total bar load in kg, or null when never declared. Read-only; from the value passed at create time (#1325). Seeds the reprocess dialog so a weightlifting re-run keeps the declared load instead of silently losing it (the repoint clears stale meta by default).

number format: double
nullable
walk_test_distance_m
required

The declared walk-test course in metres, or null when the session has none (#1493). Read-only. Seeds the reprocess dialog so a Timed Walk re-run keeps the measured course — and so a course typed wrong at upload can be corrected there instead of re-filming a perfectly good walk.

number format: double
nullable
has_scuba_meta
required

True when this session already has a scuba session-meta sidecar on Session.meta.scuba. A boolean only — the clinical payload (side / condition / arms-engaged) is never exposed. The reprocess dialog gates on it: a scuba re-run that already has a sidecar carries the stored one over (no blind overwrite); one without it (legacy / re-routing in from another activity) must supply it (#775).

boolean
report_count
required

How many active Solution reports bind this session — the reverse of the report→sessions link, shown as a ‘used in N reports’ badge on the journal row (#1192). Only populated on the session-list endpoint (the view seeds a per-page map to avoid N+1); null everywhere else, where the FE simply omits the badge.

integer
nullable
reports
required

The active Solution reports that bind this session, each with a ready name + slug — powers the ‘used in N reports’ links on the single-session player (#1192). Only populated on the session-detail endpoint (the view seeds the reverse-scan for the one session); an empty list elsewhere, where the FE shows no links.

Array<object>
object
id
required
string
name
required
string
solution_slug
required
string
movement_guide
required

Movement Norms Library entry for this session’s movement (reference table + explanation), localised to the session’s locale. Populated ONLY on the public shared payload (#1118) so the /r/ report renders the guide without a second authenticated call; null on the authenticated app (which calls GET /api/movement-guide/) and when the library has no entry.

object
activity
required
string
exercise
required
string
what_it_measures
required

Plain-English description of the movement and what its variables capture.

string
key_fact
required

One evidence-backed fact, with citation where available.

string
demographics
required

How the bands shift by sex/age.

string
skill
required

What improves from beginner to advanced.

string
metrics
required

Per-metric reference row for the Movement Guide table. min_good/max_good are the team-default reference band (same as the Threshold configurator) in the metric’s canonical unit; evidence_basis is validated | experimental | assumed; reference_only metrics are library variables we don’t yet compute for a session.

Array<object>
object
key
required
string
label
required
string
unit
required
string
higher_is_better
required
boolean
nullable
min_good
required
number format: double
nullable
max_good
required
number format: double
nullable
evidence_basis
required
string
source
required
string
note
required
string
per_athlete
required
boolean
reference_only
required
boolean
Example
{
"activity": "running",
"status": "pending"
}
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"
}
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"
}
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
Examples
Example404—NoSuchSessionOnThisTeam

Absent, or owned by another team (collapsed to 404)

{
"detail": "session_not_found"
}
Media typeapplication/json
object
detail
required

409 — the request conflicts with current state. detail is a stable code (e.g. reanalyze_in_flight, already_on_plan, email_in_use). Some 409s carry extra context fields alongside detail; those are documented on the specific endpoint that emits them.

string
Examples
Example409—SessionNotInAReviewableState

Review rejected while the session is still pending or processing

{
"detail": "session_not_reviewable"
}