Analyse a bout's touches
const url = 'http://localhost:8000/api/bouts/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/analyse/';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"lang":"en"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url http://localhost:8000/api/bouts/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/analyse/ \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "lang": "en" }'Turns every included touch without a session into a fencing Bout session over its window. The first call debits the bout’s quota units once (402 when the team has fewer); later calls, for touches added since, debit nothing. A bout analyses at most the configured number of touches. A bout whose every touch session fails gets its units back.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Request Body
Section titled “Request Body”object
The coach’s UI language: the touch videos’ angle labels are drawn in it, as for a session upload.
en- Englishru- Русскийes- Español
object
The coach’s UI language: the touch videos’ angle labels are drawn in it, as for a session upload.
en- Englishru- Русскийes- Español
object
The coach’s UI language: the touch videos’ angle labels are drawn in it, as for a session upload.
en- Englishru- Русскийes- Español
Responses
Section titled “Responses”object
object
object
Send it back with a touch edit; a list that has moved since answers 409.
object
The touch’s pair metrics and our fencer’s footwork and hand, number or category text by key (bout_distance_at_attack_m, bout_first_to_close, bout_strip_position_m, bout_first_to_extend, bout_extension_lead_ms, step_count, advance_count, retreat_count, step_rate, weapon_hand_peak_speed).
object
object
Touches whose analysis came back with metrics.
object
Null until a touch reads it.
object
Null until a touch reads it.
object
Advances over the analysed touches, before each attack; null with none read.
Retreats over the analysed touches; null with none read.
Steps per minute, mean over touches.
object
Foil when a white lamp was read on any touch (only foil has an off-target lamp); null when the lamps cannot tell. Never overrides the declared weapon.
Sessions of quota the bout’s one Analyse debits.
The longest period video the lamp read takes, seconds; the uploader checks it before uploading.
Examples
A bout
{ "id": "6d0f3c2a-1b4e-4c8a-9f21-7e5d3a9b0c11", "client": null, "name": "Pool vs Smith", "weapon": "foil", "handedness": "right", "opponent_handedness": "", "ours_side": "left", "periods": [ { "id": "a3e1c9f0-52b7-4d16-8c0e-91f4b2d7e6a5", "index": 0, "video_url": "https://media.example.com/teams/9b1c7e2a/sessions/2026/10/0c5b1d7e-6a2f-4e3b-8d19-5f0a7c2e4b61.mp4", "duration_s": 312.4, "calibration": { "lamps": [ [ 0.44, 0.49, 0.47, 0.52 ], [ 0.51, 0.49, 0.54, 0.52 ] ] }, "detect_status": "done", "detect_error": "" } ], "touches": [ { "id": "3f2a9c1d0b7e", "period": 0, "onset_s": 13.8, "lamps": [ "green", "red" ], "window_s": [ 7.8, 14.8 ], "double": true, "source": "lamps", "include": true, "ours_declared": null, "ours": "left", "check_sides": false, "session_id": null } ], "touches_version": "5be1c0a92f3d7e64", "touch_sessions": [], "quota_units": 3, "max_period_s": 480, "aggregates": { "touches": 1, "analysed": 0, "doubles": 1, "opponent_handedness": null, "lamps": { "red": 1, "green": 1, "white": 0 }, "distance_at_attack_mean_m": null, "first_to_close": null, "first_to_extend": null, "extension_lead_mean_ms": null, "steps_per_touch_mean": null, "advances": null, "retreats": null, "step_rate_mean": null, "weapon_hand_peak_speed_mean_m_s": null, "weapon_hand_peak_speed_max_m_s": null, "strip_positions": [], "weapon_suggestion": null, "weapon_mismatch": false }, "analysed_at": null, "created_at": "2026-10-26T10:00:00Z"}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
A side to confirm
{ "detail": "confirm which side our fencer is on where the list asks"}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
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 0subscription_suspended- Stripe dunning in flight (past_due / unpaid)seat_limit_reached- Adding another team member would exceed plan.max_seatsclient_limit_reached- Adding another Client would exceed plan.max_client_profilesfeature_not_in_plan- The tier does not include this whole feature surfaceexercise_not_in_plan- The (activity, exercise) is not in the team’s exercise grantstrial_expired- The team is read-only: its self-serve trial endedaccess_ended- The team is read-only: its paid plan or granted access ended
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).
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.
The active plan’s role string. See current_plan.
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).
Example
{ "detail": "quota_exhausted"}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
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"}