Skip to content

Uploading a video

Three calls, in this order. The middle one does not go to the API at all.

sequenceDiagram
    participant You
    participant API
    participant Storage
    You->>API: POST /api/sessions/upload-intent/
    API-->>You: session_id + presigned url
    You->>Storage: PUT the raw bytes
    Note over You,Storage: The video never touches the API
    Storage-->>You: 200
    You->>API: POST /api/sessions/
    API-->>You: 201 pending — quota debited

The video never passes through the API. upload-intent hands you a URL that is write-only to one object, you PUT the bytes there directly, and then you tell the API where they landed. Large files never touch the API’s request path.

Create the session soon after the PUT. An upload that no session has claimed after three days is deleted, and a create after that answers 400 clip_not_found.

MP4, MOV and WebM. The content_type you declare must be a video/* type (or application/octet-stream); anything else is rejected when you ask for the intent.

Up to 500 MB and 180 seconds. The PUT itself cannot refuse a bigger file — a presigned PUT carries no size range — so the check runs when you create the session: it measures the uploaded object and answers 400 clip_too_large or clip_too_long (or clip_not_found if the PUT has not finished) before any quota is touched. With a trim window, the window is what counts. A trim window must be at least 1 second long (end_s − start_s ≥ 1); a shorter one is a 400 with a field error under trim.end_s.

A session that renders with original_quality: true has a shorter duration ceiling: 30 seconds, measured the same way — on create, and on a re-run that turns the flag on or keeps it. Over it is the same 400 clip_too_long, with max_duration_s: 30 — trim the clip, or turn the flag off. Why 30: the full-resolution render is bounded by frames — it is written at up to 30 fps, and twice when blur_face is on — and 30 seconds is what fits its frame budget with both. Read the live value from max_duration_s; it may be raised.

Shorter is better. Film the reps you want analysed, not a long clip with idle time either side — 60 seconds or less is the sweet spot, and shorter clips come back faster. Codecs, resolution and frame rate are covered in Limits and capacity.

Re-send the same presigned URL. It stays valid for two hours, and the window is deliberately sized off a slow connection rather than a round trip. Asking for a fresh intent instead gives you a new session_id, which means the old one is stranded.

If the create call’s response was lost and you retry it, you will get {"id": ["session_id_taken"]}. That is the id doing its job — check whether the session exists before re-creating it.

Terminal window
curl -X POST https://api.ai.aikynetix.app/api/sessions/ \
-H 'Authorization: Bearer aik_yourPartnerTokenHere' \
-H 'Content-Type: application/json' \
-d '{
"id": "5c836d7d-3301-49df-bfa0-9cff0550fd0e",
"activity": "running",
"exercise": "Treadmill",
"video_url": "https://s3.ai.aikynetix.app/…/5c836d7d….source.mp4",
"client": "3f1a…",
"camera_view": "side",
"name": "Anna — baseline — side view"
}'

id is the session_id from the intent; video_url is its public_url. Passing both back is what keeps the row and the stored object aligned. The public_url arrives presigned so you can read the object back; send it as it is — we store it without the signature.

These are silently dropped where they do not apply, so a multi-activity uploader can always send them:

Field Applies to If omitted
barbell_mass_kg weightlifting Mass-dependent lift metrics are suppressed rather than guessed from an assumed load.
treadmill_incline_deg running, walking Treated as level ground.
approach_step_count bowling Auto-detected.
walk_test_distance_m walking / Timed Walk Required there — the exercise has no meaning without a measured course.
slow_motion_capture_fps american_football / Long Snap 120 or 240 for a phone slow-motion clip. Omitted on a slow-motion clip, its times and speeds are withheld; on a real-time clip it is not needed.
camera_distance_m the single-leg hops: jump / Single Leg Hop and Return, Single Leg Triple Hop, Single Leg Broad Jump, and the last two under american_football The analyzer assumes the recording guide’s protocol distance (2.75 m). Measured lens-to-athlete distance at the start position, 1–20 m, any camera view.
handedness fencing, tennis, bowling Assumes right-handed.
trim any The whole clip is analysed.
blur_face any Blurred: faces are blurred in the analysed video. Send false for no blurring. The raw upload is never blurred either way.
original_quality any The analysed video comes back at the default 1920-pixel long side. See below.

original_quality is an optional boolean. true delivers the analysed video (analyzed_url) at the clip’s own resolution, up to 4K (3840 long side) at up to 30 fps, instead of the default 1920 long side, at a higher encode quality (a clip analysed at 60 fps comes back at 30) — for presentations and marketing material. A Full HD or smaller clip comes back at its own size (never upscaled). It changes only the video: metrics are not affected. It costs noticeably more processing time and a file several times larger, and the clip is held to the 30 seconds ceiling above. The session read returns original_quality so you can tell the two apart; POST /api/sessions/{id}/reanalyze/ accepts it too (true / false; omit it to keep the session’s current choice). The read also says what the last completed run delivered, original_quality_rendered: false when the video fell back to the default render (a re-run may succeed), null when the run did not ask and while no completed run exists, e.g. a re-run in flight (original_quality_anonymized_rendered is the same for the face-blurred copy).

Two activities require extra payloads rather than accepting them optionally: scuba_diving needs its session-meta sidecar, and workspace_wellness / Ergonomics Assessment needs the workstation survey. Both are rejected at create time if missing, because neither analysis is meaningful without them.