A presigned PUT for one clip
const url = 'http://localhost:8000/api/portal/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/uploads/intent/';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"content_type":"video/mp4","size_bytes":48211337,"filename":"run.mp4"}'};
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/portal/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/uploads/intent/ \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "content_type": "video/mp4", "size_bytes": 48211337, "filename": "run.mp4" }'Upload the clip to url with method and headers, then send session_id to POST …/uploads/ within 45 minutes (the PUT is signed for 30, plus a margin). The PUT must carry exactly size_bytes bytes: the size is signed. 400 for a content type that is not video/* or application/octet-stream, or clip_too_large (max_bytes) past the upload limit. 409 upload_cap_reached when this month’s cap is used up — an intent not yet sent holds its slot until it lapses (checked again at the create); 429 upload_attempts_exceeded past 10 intents a day; 403 access_ended team_plan while the team cannot run analyses.
The Contribute level only: a View grant gets 403 contribute_required.
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”Request Bodyrequired
Section titled “Request Bodyrequired”object
MIME type of the clip; replayed as the PUT Content-Type. video/* or application/octet-stream only, as the coach’s upload intent (#1735): anything else is 400.
The clip’s size in bytes (File.size). Checked against the upload limit, then signed into the PUT: storage refuses a body of any other length.
Never used as the storage key.
Examples
A clip
{ "content_type": "video/mp4", "size_bytes": 48211337, "filename": "run.mp4"}object
MIME type of the clip; replayed as the PUT Content-Type. video/* or application/octet-stream only, as the coach’s upload intent (#1735): anything else is 400.
The clip’s size in bytes (File.size). Checked against the upload limit, then signed into the PUT: storage refuses a body of any other length.
Never used as the storage key.
Example generated
content_type=example&size_bytes=1&filename=exampleobject
MIME type of the clip; replayed as the PUT Content-Type. video/* or application/octet-stream only, as the coach’s upload intent (#1735): anything else is 400.
The clip’s size in bytes (File.size). Checked against the upload limit, then signed into the PUT: storage refuses a body of any other length.
Never used as the storage key.
Responses
Section titled “Responses”PortalUploadIntent: a presigned PUT for one clip.
object
The upload id; send it back to create.
object
Examples
{ "session_id": "7c1e2d3f-4a5b-4c6d-8e7f-90a1b2c3d4e5", "url": "https://s3.example.com/aikynetix-media/teams/t/sessions/2026/11/u.source.mp4?X-Amz-Signature=…", "method": "PUT", "headers": { "Content-Type": "video/mp4" }, "public_url": "https://s3.example.com/aikynetix-media/teams/t/sessions/2026/11/u.source.mp4"}A stable code (clip_too_large), or errors keyed by field.
A stable code in detail (and code), with its limit.
object
Validation errors keyed by field — each key an offending field of the body, each value its human-readable messages (or a stable code, e.g. not_editable).
object
Examples
400 — too large
{ "detail": "clip_too_large", "code": "clip_too_large", "max_bytes": 524288000}400 — not a video
{ "content_type": [ "Unsupported video content type 'text/html'. Expected a `video/*` MIME type (or `application/octet-stream`)." ]}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
{ "detail": "access_ended", "reason": "team_plan", "client_name": "Sofia Lebedeva", "team_name": "North Track Academy"}403 — a View grant
{ "detail": "contribute_required"}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
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.
Examples
409 — cap reached
{ "detail": "upload_cap_reached"}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.
Examples
429 — too many attempts today
{ "detail": "upload_attempts_exceeded"}