Released results of a card
const url = 'http://localhost:8000/api/portal/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/sessions/';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url http://localhost:8000/api/portal/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/sessions/ \ --header 'Authorization: Bearer <token>'The card’s released sessions, paginated (limit / offset), newest first: each as its light row (scalars, no series), curated, with released_at. Filters: activity, exercise, search (name, exercise or activity, any language), the date window and sort (newest · oldest).
The signed-in person’s grant on the card in the path. Every route answers 404 while client access is switched off on the environment. 404 card_not_found for a card the account never held — another athlete’s, a card of the account’s own team, or an id that names nothing. 403 access_ended (reason: revoked · team_plan · minor) for a card it held and can no longer open, consent_required when the consent texts changed since the person accepted them (consent again with POST /api/portal/{client_id}/consent/). A partner API key gets 403. 429 past the account’s rate (client_portal, 120/min; the PDFs client_portal_pdf, 20/min).
Only what a coach released is visible, curated on the server: the team’s hidden metrics are absent, the coach’s AI text and internal fields are not sent, and media URLs are signed for two hours.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Query Parameters
Section titled “Query Parameters”First day, inclusive (YYYY-MM-DD).
en · ru · es; default the grant’s language.
newest (default) · oldest.
Last day, inclusive (YYYY-MM-DD).
The reader’s IANA zone for the days.
Responses
Section titled “Responses”The limit/offset envelope over PortalResultRow.
object
One row of the results list (PortalResultRow): the session’s light
row (scalars only, no series), curated, with the portal’s marks.
object
Server-assigned UUID. Use the same value as session_id from the upload-intent step to keep the storage object key consistent.
Activity family — running, walking, jumps, weightlifting, agility, mobility, fencing, ergonomics. Drives which analyzer runs.
running- Runningwalking- Walkingjump- Vertical Jumpweightlifting- Weightliftingmobility- Mobility Assessmentworkspace_wellness- Workspace Wellnessagility- Agilityfencing- Fencingcycling- Cyclingtennis- Tennispadel- Padel Tennisbowling- Bowlingbaseball- Baseballfootball- Footballbadminton- Badmintoncricket- Cricketbasketball- Basketballscuba_diving- Scuba Divingice_hockey- Ice Hockeyamerican_football- American Footballfri- Falls Risk Indicatorpickleball- Pickleball
Specific movement within the activity (e.g. Treadmill, Back Squat, Single-Leg Stand). Pick from GET /api/exercises/?activity=....
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.
Lifecycle state: pending (queued) → processing (analyzer running) → completed (metrics + annotated mp4 ready) | failed (see failure_reason).
pending- Pendingprocessing- Processingcompleted- Completedfailed- Failed
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.
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.
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.
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.
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.
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.
Source video duration in milliseconds.
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.
ISO-8601 UTC timestamp of session creation.
UUID of the Client (athlete) the session is about. Optional but strongly recommended — without a client, body-aware metric adjustments fall back to defaults.
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.
Human-readable explanation when status == 'failed'. Empty otherwise.
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'.
object
object
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
One entry of metric_targets — schema only; the payload is
domain.target_bands.TargetBand.as_payload.
object
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
group: the band is a client group’s own range.
group- group
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.
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.
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.
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.
object
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
limb- limblabel- label
resolved- resolvedneeds_side- needs_sideconflict- conflictunscoped- unscoped
object
left- Leftright- Rightboth- Both
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.
object
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.
object
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).
object
untested_side- untested_sidenon_target_joint- non_target_jointside_unresolved- side_unresolvedsubject_switched- subject_switched
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).
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.
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
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.
0-based index of stage in the worker’s stage order.
Frames processed so far inside the current stage. Only moves during stages with sub-progress (today: pose_extract); 0 elsewhere.
Total frame count for the current stage; 0 when the stage has no frame counter or the worker hasn’t computed it yet.
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’.
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.
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.
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.
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.
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.
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.
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.
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).
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
One normalised weightlifting peak (#2379) — schema only.
object
bw- bww_per_kg- w_per_kg
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.
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.
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).
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.
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.
The DECLARED weightlifting variant (#2379) — squat / power for Clean, Snatch, Clean & Jerk; conventional / sumo for Dead Lift — or null when undeclared. Seeds the reanalyze dialog. A squat catch scores its catch depth; a power one shows it without a zone; undeclared reports it unscored with the zone.
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.
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).
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
Trim start in seconds from the clip origin.
Trim end in seconds; must be greater than start_s by at least 1 s.
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).
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.
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.
object
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/
object
Plain-English description of the movement and what its variables capture.
One evidence-backed fact, with citation where available.
How the bands shift by sex/age.
What improves from beginner to advanced.
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.
object
Released (or shared again) and not opened by this person since.
Examples
A page
{ "count": 1, "next": null, "previous": null, "results": [ { "id": "5e2b9c41-7d3a-4f80-9b6e-1c0d2e3f4a51", "client": "a1f1c0a2-0000-4000-8000-0000000000aa", "activity": "running", "exercise": "Treadmill", "name": "", "seq_no": null, "status": "completed", "failure_reason": "", "created_at": "2026-11-02T09:40:00Z", "processed_at": "2026-11-02T09:41:30Z", "released_at": "2026-11-02T10:15:00Z", "uploaded_by": null, "locale": "en", "reader_mode": "coach", "is_demo": false, "video_url": "https://s3.example.com/aikynetix-media/teams/t/sessions/2026/11/5e2b9c41-7d3a-4f80-9b6e-1c0d2e3f4a51.source.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=7200&X-Amz-Signature=…", "analyzed_url": "https://s3.example.com/aikynetix-media/teams/t/sessions/2026/11/5e2b9c41-7d3a-4f80-9b6e-1c0d2e3f4a51.analyzed.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=7200&X-Amz-Signature=…", "thumbnail_url": "https://s3.example.com/aikynetix-media/teams/t/sessions/2026/11/5e2b9c41-7d3a-4f80-9b6e-1c0d2e3f4a51.thumb.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=7200&X-Amz-Signature=…", "anonymized_url": "", "pose3d_url": "", "blur_face": false, "original_quality": false, "original_quality_rendered": null, "original_quality_anonymized_rendered": null, "source_deleted": false, "source_duration_s": 24.6, "slow_motion_capture_fps": null, "trim": null, "camera_view": "side", "camera_distance_m": null, "handedness": null, "handedness_confirmed": false, "stance_leg": null, "free_leg_position": null, "lift_variant": null, "barbell_mass_kg": null, "weapon": null, "walk_test_distance_m": null, "sprint_splits_s": null, "target_scope": null, "has_scuba_meta": false, "has_active_share": null, "report_count": null, "reports": [], "progress": null, "capture_findings": null, "normalised_peaks": null, "metrics": [ { "key": "score", "value_num": 80, "value_json": null }, { "key": "cadence", "value_num": 172, "value_json": null } ], "metric_targets": { "cadence": { "min_good": 165, "max_good": 185, "amber_low": null, "amber_high": null, "higher_is_better": true, "beta": false, "precision_beta": false, "source": null } }, "low_confidence_metrics": [], "unmeasured_metrics": [], "withheld_metrics": null, "movement_guide": null, "has_report_pdf": true, "is_new": 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).
Example generated
{ "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).
Example generated
{ "detail": "example"}object
access_ended, consent_required, or DRF’s message for a partner API key.
With access_ended: why the card no longer opens — revoked, team_plan or minor.
With consent_required: the version to accept.
With consent_required: the texts to show.
Examples
403 — access ended
The card no longer opens; the page says why and names nothing else of it.
{ "detail": "access_ended", "reason": "revoked", "client_name": "Sofia Lebedeva", "team_name": "North Track Academy"}403 — consent required
{ "detail": "consent_required", "consent_version": "2026-10-29-draft", "kinds": [ "terms", "privacy", "processing", "guardian_declaration" ]}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 card of this account
{ "detail": "card_not_found"}object
429 — throttled: this account’s (or address’s) rate for the route is used up; try again after wait seconds, which Retry-After repeats. A route with a limit of its own answers its own code instead, documented there.
With throttled: whole seconds until the next request can pass.
Example generated
{ "detail": "example", "wait": 1}