Skip to content

Re-run the analysis pipeline for a session

POST
/api/sessions/{id}/reanalyze/
curl --request POST \
--url http://localhost:8000/api/sessions/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/reanalyze/ \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "trim": { "start_s": 2.4, "end_s": 9.8 } }'

Re-queues the pose pipeline against the existing video. Useful when the analyzers have been updated since the original run (new metric, fixed bug, model bump). Sets status back to pending and clears the previous analyzed_url so clients fall back to the raw upload until the worker repopulates it. Atomic — a session that’s already queued (pending) or running (processing) returns 409 reanalyze_in_flight rather than spawning a second worker that would race on metric writes. A session whose unblurred original was deleted under its team’s setting (source_deleted: true) returns 409 source_deleted before anything changes: a re-run needs the original. Returns 202 with the reset Session; poll as you would after POST /api/sessions/.

Re-route (optional, #775): pass activity + exercise (both, together) to re-derive the session under a DIFFERENT analyzer — for a misclassified capture or a clip that should now run as a newly-shipped exercise. The pair is validated against the taxonomy (400 on an invalid pair); stale metrics from the old exercise are cleared (coach notes preserved). Omit both to re-run under the current activity.

Reassign the client (optional, #923): pass client_id to move the session to another of the team’s clients before the re-run — the pipeline then recomputes the body-aware metrics + green zones against the new client’s profile (height / weight / gender / leg length). null unassigns. An unknown id or another team’s client is 400 client_not_found. Valid with or without the re-route pair — a client-only body re-runs under the current pair.

Re-cut the clip (optional, #2250): pass trim ({start_s, end_s}, seconds from the origin of the ORIGINAL upload) to change the cut before the re-run — the same server-side cut the upload wizard applies, so nothing is re-encoded or re-uploaded and the session keeps its id, coach review, share links and history. null analyzes the whole clip. Key-presence semantics: omit the key and the stored cut rides through untouched on every path, including a re-route. An inverted, non-finite or too-short range is 400 with field errors nested under trim; one past the end of a clip whose length the server knows (source_duration_s on the session read) is 400 with trim, code: trim_beyond_source and source_duration_s as data. Valid with or without the re-route pair.

Original quality (optional, #2391): original_quality: true re-renders the analysed video at the clip’s own resolution, up to 4K, instead of the default 1920 long side; false goes back to the default render; omit it to keep the session’s current choice (original_quality on the session read). Metrics are unaffected. Valid with or without the re-route pair.

Clip gate. A re-run is held to the same ceilings as create, measured on the window it will analyse — 400 clip_too_long / clip_too_large / clip_not_found / content_blocked, same body — and one that moves the session to another activity to the activity-mismatch gate, 400 activity_mismatch / activity_mismatch_confirm (#2801, see create). A re-run that will render in original quality (sent true, or omitted on a session that already has it) answers to the shorter original-quality ceiling, 30 s by default, carried as max_duration_s.

Quota. A session still holding its original create debit re-runs for free — that is the common case, and the ledger doesn’t move. A session that FAILED released its slot at failure time (so the failed attempt cost nothing), and the re-run re-takes it. That only fails when the slot was spent elsewhere in the meantime: 402 quota_exhausted, same payload as POST /api/sessions/. Retrying a clip our own pipeline just broke therefore never hits the paywall — the slot being re-taken is the one that failure returned.

id
required
string format: uuid

Optional body for POST /api/sessions/{id}/reanalyze/ (#775).

Omit everything to re-run under the session’s CURRENT activity (the default reanalyze). Pass activity + exercise (both) to re-derive the session under a DIFFERENT analyzer — a misclassified capture, or a clip that should now run as a newly-shipped exercise. Either way you may also pass the analysis metadata the chosen activity needs — handedness (dominant hand / foot), camera_view, or the scuba_session_meta sidecar — to correct it or supply it for the new analyzer; each is written onto Session.meta exactly as at upload, and only the keys you send are touched (the rest of meta is preserved). The pair is validated against the taxonomy server-side; the session resets to PENDING + re-runs with no new quota debit.

client_id (#923) reassigns the session to another of the team’s clients (or null to unassign) before the re-run, so the pipeline recomputes the body-aware metrics + green zones against the correct anthropometrics. It is valid on its own — no activity+exercise pair required — because a reassignment always re-runs under the current pair.

trim (#2250) re-cuts the ORIGINAL upload before the re-run — the cut a coach picked once in the wizard and, until this field, could only change by uploading the same clip again. Same key-presence semantics as client_id, and valid on its own for the same reason: a cut changes the frames every analyzer reads, never which analyzer runs.

original_quality (#2391) re-renders the session at the source resolution (true) or back at the default (false); omitted keeps the session’s current choice. Valid on its own — it changes how the result is DELIVERED, not what is analysed.

object
activity

Re-route the session to this activity. Requires exercise.

  • 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
  • pickleball - Pickleball
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 pickleball
exercise

Exercise within the new activity — must be a valid preset for it (see GET /api/exercises/). Requires activity.

string
>= 1 characters <= 80 characters
activity_mismatch_confirmed

The coach’s answer to 400 activity_mismatch_confirm / activity_mismatch on a re-run that moves the session to another activity (#2801): analyse it under that activity although the clip’s vision read named another one (a different sport: team owner only, see can_override). A re-run that keeps the activity is never asked.

boolean
handedness

Dominant hand / foot for handedness-aware activities (tennis, padel, pickleball, badminton, baseball, basketball, cricket, fencing, football). Written to Session.meta.handedness; ignored by activities whose analyzers don’t read it.

  • right - Right-handed
  • left - Left-handed
string
Allowed values: right left
handedness_confirmed

The coach checked handedness against the clip (#2551). The tennis Serve then never switches the analysis to the other hand; it still flags a contradiction. Requires handedness. Cleared on every reprocess that does not send it again, so a confirmation never outlives the hand it confirmed.

boolean
camera_view

Camera plane. front / back swap to the frontal analyzer for the activities that have one. Must be one of the new pair’s supported views — an unsupported plane is a 400 (#1829); see views_by_exercise in GET /api/exercises/?activity=…. Written to Session.meta.camera_view.

  • side - Side
  • front - Front
  • back - Back
string
Allowed values: side front back
target_side

The tested side when re-routing to a unilateral mobility test (#2354). Omit to let the analyzer read it. To change the side of an analysed session without re-running it, use PATCH /api/sessions/{id}/target-scope/.

  • left - Left
  • right - Right
  • both - Both
string
Allowed values: left right both
scuba_session_meta

Scuba sidecar — required when re-routing to scuba_diving, ignored otherwise. Written to Session.meta.scuba.

object
side_involved
required
One of:
  • L - Left
  • R - Right
string
Allowed values: L R
condition
required

§5.2 condition tag. Used for cohort tagging on the dashboard + the report PDF cover. Must match the trial design — not derived from other fields, because the same diver shoots multiple conditions per session.

  • control_no_arms - Control — no arms
  • control_with_arms - Control — with arms
  • transtibial - Transtibial
  • transfemoral - Transfemoral
string
Allowed values: control_no_arms control_with_arms transtibial transfemoral
arms_engaged
required

Confound flag (§4.6). True when the diver was sculling / using arms for propulsion during the bout. Surfaces in the report so coaches know to filter symmetry numbers.

boolean
fin_model

Fin model — free-form (manufacturer + model).

string
"" <= 128 characters
fin_stiffness

Fin stiffness. Free-form: either a qualitative grade (soft / medium / stiff) or a numeric string with units (12 N·m/rad). Analyzer doesn’t consume it in V1; captured for the report + future fin-deflection v2 work.

string
"" <= 64 characters
borg_residual_post_bout

Borg CR-10 perceived exertion (residual side), 0–10.

integer
nullable <= 10
borg_sound_post_bout

Borg CR-10 perceived exertion (sound side), 0–10.

integer
nullable <= 10
calibration_scale_bar_m

Length of the in-frame calibration scale bar (§6.1), in metres. Optional — without it, body-aware distance metrics use body_scale_px(ctx) per the standard fallback.

number format: double
nullable
client_id

Reassign the session to this client before the re-run (#923) — body-aware metrics + green zones recompute against the new client’s profile. null unassigns. Must be one of the caller’s team’s clients (400 client_not_found otherwise). Unlike the analysis metadata, valid WITHOUT an activity + exercise pair: a client-only body reassigns and re-runs under the session’s current pair.

string format: uuid
nullable
barbell_mass_kg

(Re-)declare the barbell’s total load in kg for a weightlifting re-run (#1325). Key-presence semantics like client_id: a value writes Session.meta.barbell_mass_kg (an override re-run would otherwise clear it — stale meta doesn’t survive a repoint); null explicitly clears it; absent leaves the stored value alone on a plain re-run. Valid WITHOUT an activity + exercise pair — it doesn’t change WHICH analyzer runs, only an input it reads, so a mass-only body re-runs the current pair with the corrected load (e.g. a session uploaded before the load could be declared). Silently ignored when the target activity isn’t weightlifting.

number format: double
nullable >= 1 <= 500
walk_test_distance_m

(Re-)declare the measured course length in metres for a walking / Timed Walk re-run (#1493). Same key-presence semantics as barbell_mass_kg: a value writes Session.meta.walk_test_distance_m, null clears it, absent leaves the stored course alone. This is the correction path for a course entered wrong at upload — the walk itself is fine, only the number the speed is divided by was, and re-filming to fix a typo would be absurd. Valid WITHOUT an activity + exercise pair: it does not change WHICH analyzer runs, only an input it reads. Silently ignored when the target pair isn’t walking / Timed Walk.

number format: double
nullable >= 2 <= 100
camera_distance_m

(Re-)declare the measured lens-to-athlete distance in metres for a single-leg hop re-run (#622). Same key-presence semantics as walk_test_distance_m: a value writes Session.meta.camera_distance_m, null clears it (back to the protocol distance), absent leaves the stored distance alone. Valid WITHOUT an activity + exercise pair: it does not change WHICH analyzer runs, only an input it reads. Silently ignored when the target pair is not a single-leg hop.

number format: double
nullable >= 1 <= 20
sprint_split_5m_s

(Re-)declare the 5 m timing-gate split in seconds for a running / 20 m Sprint re-run (#1706). Same key-presence semantics as barbell_mass_kg: a value writes Session.meta.sprint_split_5m_s, null clears it, absent leaves the stored split alone. This is the path that matters most for this exercise: a squad that owns timing gates usually uploads the clip first and reads the times off the rig afterwards, and re-filming a sprint to attach three numbers would be absurd. All three splits or none, as on create. Valid WITHOUT an activity + exercise pair: they do not change WHICH analyzer runs, only an input it reads. Silently ignored when the target pair isn’t running / 20 m Sprint.

number format: double
nullable >= 0.5 <= 3
sprint_split_10m_s

(Re-)declare the 10 m timing-gate split in seconds. Same rules and the same storage as sprint_split_5m_s.

number format: double
nullable >= 0.8 <= 6
sprint_split_20m_s

(Re-)declare the 20 m timing-gate split in seconds. Same rules and the same storage as sprint_split_5m_s.

number format: double
nullable >= 1.5 <= 10
approach_step_count
One of:
  • 4 - 4-step
  • 5 - 5-step
  • 6 - 6-step
integer
Allowed values: 4 5 6
stance_leg
One of:
  • left - Left
  • right - Right
string
Allowed values: left right
free_leg_position
One of:
  • front - Front
  • middle - Middle
  • back - Back
  • floor_reach - Floor reach
string
Allowed values: front middle back floor_reach
weapon
One of:
  • foil - Foil
  • epee - Épée
  • sabre - Sabre
string
Allowed values: foil epee sabre
slow_motion_capture_fps
One of:
  • 120 - 120
  • 240 - 240
integer
Allowed values: 120 240
lift_variant
One of:
string
Allowed values: squat power conventional sumo
trim

Re-cut the ORIGINAL upload before the re-run (#2250): {start_s, end_s} in seconds from the clip origin, applied server-side at normalize time exactly like the upload wizard’s cut (#556) — the stored clip is never re-encoded or replaced, so the session id, coach review, share links and history all survive. Same key-presence semantics as client_id: a value writes Session.meta.trim, null clears it (the whole clip is analyzed), absent leaves the stored cut exactly as it was — a plain re-run, a re-route or a client reassignment never silently drops a cut. Valid WITHOUT an activity + exercise pair: it changes the frames every analyzer reads, not which analyzer runs. Refused with field errors under trim when the range is inverted, non-finite, shorter than the wizard’s 1 s floor, or runs past the end of the source where the server knows its length (a completed run of the whole clip).

object
start_s
required

Trim start in seconds from the clip origin.

number format: double
end_s
required

Trim end in seconds; must be greater than start_s by at least 1 s.

number format: double
original_quality

Re-render in “original quality” (#2391) — the annotated video at the clip’s own resolution, up to 4K, instead of the default 1920 long side; see original_quality on POST /api/sessions/. true turns it on (Session.meta.original_quality), false turns it off, absent keeps the session’s current choice on every path, including a re-route. Metrics are unaffected. When the re-run will render in original quality (sent true, or omitted on a session that already has it) the clip — or the trim window the re-run will analyse — is held to the shorter original-quality ceiling (30 s by default): 400 clip_too_long with that ceiling as max_duration_s. Valid WITHOUT an activity + exercise pair: it changes how the result is delivered, not which analyzer runs.

boolean

Examples

ExampleRe-cutTheClip,SameActivity

A trim-only body re-runs the current pair on a new cut of the original upload (#2250)

{
"trim": {
"start_s": 2.4,
"end_s": 9.8
}
}
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
  • pickleball - Pickleball
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 pickleball
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

Presigned URL (see video_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

Presigned URL (see video_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

Presigned URL (see video_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

Presigned URL (see video_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, precision_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); precision_beta is the part of it that is about this session’s reading (a frame rate below what the read needs, or a pose-model fallback) rather than the zone. source names the client group when the athlete’s group range won the band (#2518), else null. Empty list when the activity has no zone-defining specs.

object
key
additional properties

One entry of metric_targets — schema only; the payload is domain.target_bands.TargetBand.as_payload.

object
min_good
required
number format: double
nullable
max_good
required
number format: double
nullable
amber_low
required
number format: double
nullable
amber_high
required
number format: double
nullable
higher_is_better
required
boolean
nullable
beta
required
boolean
source
required

Set when a client group’s own range won this band (#2518) — render “Range from group «{group_name}»”, or the neutral “Group range” when group_name is empty (public share links). null for every band resolved from the team / platform cohorts or the spec.

object
kind
required

group: the band is a client group’s own range.

  • group - group
string
Allowed values: group
group_id
required

The client group whose range won. null on the public share payloads (/api/shared/…: the shared player and the public comparison), which withhold the group — its id and its name.

string format: uuid
nullable
group_name
required

That group’s name, for the label. "" on the public share payloads (the shared player and the public comparison): a squad’s name can be health information, and the reader shows the neutral “Group range” note instead.

string
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 / subject_switched (the skeleton changed person mid-clip; the score is withheld, #2709) / frame_rate_low (filmed below 45 fps — a 24 / 25 / 30 fps capture — for an activity analysed at 60, bowling excepted; data.source_fps). Severity: advisory recommends re-recording the clip; note qualifies some numbers without asking for a re-record (frame_rate_low); info is not shown. Fatal capture problems FAIL the session and speak through failure_reason.

Array<object>
nullable
object
key
additional properties
target_scope
required

The tested side (and target joints) of a unilateral mobility test (#2354), or null for a test that is not scoped: {mode, status, side, source, requested, confirmed, detected, joints, core, sides, abstained, score_include, score_exclude, score_set_saved}. score_set_saved says whether the score edits are the viewing coach’s saved default for the test. status is resolved (metrics are the tested side’s), needs_side (the analyzer could not tell and nothing was picked — no metrics, no score until the coach picks), conflict (the coach’s pick and a confident analyzer read disagree — same), or unscoped (no side known, the full two-sided report). source says who decided the side (user / ai); detected is the analyzer’s read with its 0-1 confidence. core lists the test’s core metric keys, shown first. Change it with PATCH /api/sessions/{id}/target-scope/.

object
mode
required
  • limb - limb
  • label - label
string
Allowed values: limb label
status
required
  • resolved - resolved
  • needs_side - needs_side
  • conflict - conflict
  • unscoped - unscoped
string
Allowed values: resolved needs_side conflict unscoped
side
required
One of:
  • left - Left
  • right - Right
  • both - Both
string
Allowed values: left right both
source
required
One of:
  • user - user
  • ai - ai
string
Allowed values: user ai
requested
required
One of:
  • left - Left
  • right - Right
  • both - Both
string
Allowed values: left right both
confirmed
required
boolean
detected
required
object
side
required
  • left - Left
  • right - Right
  • both - Both
string
Allowed values: left right both
confidence
required
number format: double
<= 1
abstained
boolean
joints
required
Array<string>
Allowed values: shoulder elbow wrist trunk hip knee ankle
core
required
Array<string>
sides
Array<string>
Allowed values: left right both
score_include
required
Array<string>
score_exclude
required
Array<string>
score_set_saved
required
boolean
low_confidence_metrics
required

Shown metrics the analyzer still emits but trusts less (#2527): [{key, reasons}], one entry per scalar in metrics whose withheld_reads entry is low_confidence, reason codes root cause first (e.g. camera_moving, slide_end_not_seen, low_frame_rate). The player marks the card and prints the reason, localised on the client. [] when the session has none.

Array<object>
object
key
required
string
reasons
required
Array<string>
unmeasured_metrics
required

Cards the analyzer declined to publish on this session (#2643): [{key, reasons}], one entry per metric of this movement whose withheld_reads entry is not_available and which has no value here (e.g. slow_motion_rate_unknown on a slow-motion Long Snap uploaded without its capture rate). The player says which cards are missing and why, localised on the client. [] when none.

Array<object>
object
key
required
string
reasons
required
Array<string>
withheld_metrics
required

What the analyzer measured but this session does not report (#2354): [{key, value_num, value_json, reason}], reason untested_side / non_target_joint / side_unresolved / subject_switched (#2709, the score only). Never scored — the player’s ‘Show all tracked joints’ list. null means NOT REPORTED, which is every response but the session detail (a per-row query would cost the list one query per session).

Array<object>
nullable
object
key
required
string
value_num
required
number format: double
nullable
value_json
required
nullable
reason
required
  • untested_side - untested_side
  • non_target_joint - non_target_joint
  • side_unresolved - side_unresolved
  • subject_switched - subject_switched
string
Allowed values: untested_side non_target_joint side_unresolved subject_switched
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
handedness_confirmed
required

True when the coach confirmed handedness against the clip in Reprocess (#2551); the tennis Serve then never switches off it. Seeds the reprocess dialog’s confirmation box.

boolean
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
reader_mode
required

coach | amateur — whose words this session’s results are written in (#2867). On the public GET /api/shared/{token}/ it is the mode frozen on the link; elsewhere it is the client’s own setting, which the player opens in and the coach can toggle. Always coach for an activity with no amateur copy.

string
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
original_quality
required

True when this session renders its analysed video in “original quality” (#2391) — at the clip’s own resolution, up to 4K, instead of the default 1920 long side. Set at create or by a re-run (POST /api/sessions/{id}/reanalyze/), which is also what seeds the reprocess dialog’s choice. Render metadata only: no metric depends on it. Read-only; false for every session without the flag.

boolean
original_quality_rendered
required

What the last completed run of an original_quality session delivered (#2391): true when the analysed video (analyzed_url) came back at the clip’s own resolution, false when it fell back to the default 1920 render — a re-run may succeed. null when the run did not ask for it, and while no completed run exists (a re-run in flight, a failed one). Read-only.

boolean
nullable
original_quality_anonymized_rendered
required

original_quality_rendered for the face-blurred copy (anonymized_url), which can fall back on its own (#2391). null when the run did not ask for original quality or delivered no blurred copy, and while no completed run exists. Read-only.

boolean
nullable
source_deleted
required

True when the unblurred original was deleted after the blurred analysis, under the team’s delete_unblurred_originals setting (#2752): video_url is empty, the blurred analyzed_url and anonymized_url remain, and the session can no longer be re-analysed (POST …/reanalyze/ answers 409 source_deleted). Read-only.

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

Weightlifting force / power normalised (#2379): {lift_peak_force | bar_peak_power: {value, unit}}, unit bw (multiples of system weight) or w_per_kg (per kg of athlete). Null when the analyzer emitted neither — it only does off a declared athlete weight.

object
key
additional properties

One normalised weightlifting peak (#2379) — schema only.

object
value
required
number format: double
unit
required
  • bw - bw
  • w_per_kg - w_per_kg
string
Allowed values: bw w_per_kg
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
camera_distance_m
required

The declared lens-to-athlete distance in metres at a single-leg hop’s start position, or null when none was measured (#622) and the analyzer used the protocol distance. Read-only. Seeds the reprocess dialog so a hop re-run keeps the measured distance.

number format: double
nullable
stance_leg
required

The DECLARED stance leg of a Single-Leg Stand or Single-Leg Squat (left / right), or null when the leg was detected from the clip (#2297, #3060). Seeds the reanalyze dialog so a correction keeps the declaration by default. What the analysis actually used is the balance_single_leg_stance_side metric (Single-Leg Stand), with balance_single_leg_stance_confidence = declared when it came from here, or sls_stance_side (Single-Leg Squat).

string
nullable
free_leg_position
required

The DECLARED free-leg position of a Single-Leg Squat (front, middle, back, floor_reach), or null when not declared (#3060). Seeds the reanalyze dialog. What the side capture read is the sls_free_leg_position metric.

string
nullable
slow_motion_capture_fps
required

The declared slow-motion capture rate of a Long Snap clip (120 / 240), or null for a real-time clip (#2643). Seeds the reanalyze dialog so a re-run keeps it by default.

integer
nullable
lift_variant
required
One of:
string
Allowed values: squat power conventional sumo
weapon
required
One of:
  • foil - Foil
  • epee - Épée
  • sabre - Sabre
string
Allowed values: foil epee sabre
sprint_splits_s
required

The declared 20 m Sprint timing-gate splits as [5 m, 10 m, 20 m] in seconds, or null when the session has none (#1706). Read-only, and a LIST where the write side takes three flat fields: the three only ever exist together, so a consumer that had to check three keys for one fact would invite a fourth state that cannot happen. Seeds the reprocess dialog, which is this exercise’s normal path to a profile rather than a correction one — a squad with gates uploads the clip and reads the times off the rig afterwards.

Array<number>
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
trim
required

The cut the analysis runs on — {start_s, end_s} in seconds from the origin of the ORIGINAL upload (video_url), or null when the whole clip is analyzed. Read-only here; picked in the upload wizard and re-picked through POST .../reanalyze/ with trim (#2250), which is what this field seeds: the reprocess dialog shows the current cut and opens the trim editor on it. duration_ms is the length AFTER this cut, not the source’s.

object
start_s
required

Trim start in seconds from the clip origin.

number format: double
end_s
required

Trim end in seconds; must be greater than start_s by at least 1 s.

number format: double
source_duration_s
required

Length of the ORIGINAL upload in seconds, as the pipeline probed it before applying the cut, or null when the server does not know it (a session analyzed before the stamp existed, unless a completed run of the whole clip makes duration_ms the source). What the reprocess dialog prints after the cut (“of 0:30”) and the bound a re-cut’s end_s is checked against (#2250).

number format: double
nullable
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",
"metric_targets": {
"additionalProperty": {
"source": {
"kind": "group"
}
}
},
"target_scope": {
"mode": "limb",
"status": "resolved",
"side": "left",
"source": "user",
"requested": "left",
"detected": {
"side": "left"
},
"joints": [
"shoulder"
],
"sides": [
"left"
]
},
"withheld_metrics": [
{
"reason": "untested_side"
}
],
"normalised_peaks": {
"additionalProperty": {
"unit": "bw"
}
},
"lift_variant": "squat",
"weapon": "foil"
}
Media typeapplication/json
object
detail

Machine code where a client branches — clip_too_long, clip_too_large, clip_not_found, content_blocked, activity_mismatch, activity_mismatch_confirm, … — else a message. ABSENT on a plain DRF validation error, which is field-keyed instead (the re-cut refusal nests its messages under trim).

string
code

Repeats the machine code: clip_too_long / clip_too_large / clip_not_found / content_blocked / activity_mismatch / activity_mismatch_confirm / unsupported_camera_view.

string
read_activity

The activity the clip’s vision read named, on activity_mismatch / activity_mismatch_confirm (#2801).

string
can_override

On activity_mismatch / activity_mismatch_confirm (#2801): whether the caller’s activity_mismatch_confirmed: true would get past it — always on a look-alike, only for the team owner or a partner key on a different sport.

boolean
video_url

English sentence for logs on a clip-gate refusal — a carrier, not a field error on the URL.

string
max_duration_s

The duration ceiling that applied, on clip_too_long — the shorter original-quality one when the session renders in original quality (#2391).

integer
max_bytes

The size ceiling, on clip_too_large.

integer
trim

Field errors on the re-cut range (#2250) — inverted, non-finite, too short, or running past the end of the clip. This shape carries no detail.

Array<string>
source_duration_s

The clip’s real length, on trim_beyond_source — so a client can re-offer the cut against the true duration.

number format: double
supported_views

The pair’s analysable camera views, on unsupported_camera_view.

Array<string>

Examples

`client_id` unknown, or owned by another team (collapsed to one 400)

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

One of eight 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; exercise_not_in_plan leads to a contact-us CTA — the movement list is sales-managed, no tier adds to it (#2030). trial_expired / access_ended mean the team is read-only (#2901): everything opens, nothing new is created; they lead to a subscribe CTA and always carry reset_at: null.

  • 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
  • exercise_not_in_plan - The (activity, exercise) is not in the team’s exercise grants
  • trial_expired - The team is read-only: its self-serve trial ended
  • access_ended - The team is read-only: its paid plan or granted access ended
string
Allowed values: quota_exhausted subscription_suspended seat_limit_reached client_limit_reached feature_not_in_plan exercise_not_in_plan trial_expired access_ended
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

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—Re-analysisAlreadyInFlight

409 — Re-analysis already in flight

{
"detail": "reanalyze_in_flight"
}