> ## Agent Instructions
> API access is self-serve: register at https://developers.ochy.io/register, verify the email (grants 1 free analysis credit), generate the API key in the portal, then buy prepaid credits at 2.00 EUR per analysis via Stripe Checkout. See /get-api-access for the exact funnel.
> Agents can complete every funnel step except card entry: hand the Stripe Checkout URL to the user for payment.
> Base URL: https://partner-api.ochy-prod.com. Pass the API key as `Authorization: Bearer` (preferred) or `X-API-Key`; never as a query parameter.
> One analysis consumes one credit, debited when the analysis starts. HTTP 424 with code insufficient_api_credits means the balance is empty: buy credits in the portal. If starting the analysis fails on Ochy's side, the credit is refunded automatically.
> Low-credit alerts: set a threshold and an optional webhook URL in the portal Settings tab or with PUT /settings/low-credit-alerts; an email and a credits.low event are sent once per crossing, and the alert rearms when the balance recovers.
> All endpoints use metric units: height in meters, weight in kilograms.
# Get analysis results
Source: https://docs.ochy.io/api-reference/analysis/get-analysis-results
/openapi.yaml get /analysis/{analysis_id}/results
Retrieve results for a specific analysis. The response shape and HTTP status code
depend on the current analysis status:
| Status | HTTP code | Includes |
|--------|-----------|----------|
| `pending` | 200 | `video_id`, `status`, `percentage` |
| `analyzing` | 202 | `video_id`, `status`, `percentage` |
| `success` | 200 | `video_id`, `status`, `percentage`, `thumbnail`, `output_video`, `biomechanical_analysis` |
| `success`, video expired | 200 | `video_id`, `status`, `percentage`, `video_expired: true`, `biomechanical_analysis` |
| `failed` | 400 | `video_id`, `status` |
**Data retention:** when you configure a [video retention period](/guides/webhooks-and-api-keys#data-retention)
and it has passed for this analysis, the media files are deleted: `thumbnail` and `output_video`
are omitted and `video_expired` is `true`. The biomechanical data is still returned until the
analysis data retention period passes, after which the analysis is deleted and this endpoint
returns 404.
**Language:** biomechanical texts are returned in the language the analysis was started with
(the `language` field of `POST /analysis/start`, English by default). Pass the `language` query
parameter to get them in another language without starting a new analysis.
For **side_view** analyses with `success`, `biomechanical_analysis` includes **`running_styles`**: an array of all detected styles (`title`, `animal`, `animal_explanation`, `percentage`, `summary`, `explanation`). **`title` is the unique identifier** for each style and is always one of: `Hop`, `Bounce`, `Push`, `Stick`, `Sit` (even when other fields are localized). See [search_key mapping](/guides/search-key-mapping#running-styles) for the full list of states.
Each segment's `texts` object includes **`recap`**, **`consequences`**, and **`correction`** for both side view and back view (side view may also include **`coach_tips`**). These are flattened from `segmented_explanation`; the nested key is never returned.
Each biomechanical item in `analysis_content` includes a **`search_key`**.
Use it for internal mapping to your own labels or content. See [search_key mapping](/guides/search-key-mapping) for the list of keys per analysis type (back view and side view).
Each biomechanical item also includes **`measured_value`**, the measured angle used to score the segment. `measured_value`, `range_good`, `range_okay`, and `range_bad` are always expressed in **degrees**.
Range boundaries can overlap because the scoring logic uses implicit inclusion/exclusion rules. For example, with `range_good: [0, 102]` and `range_okay: [102, 111]`, a value up to `102` is considered inside the good range, while values strictly greater than `102` are considered inside the okay range.
For **back_view**, the `explanation` field is currently not available for `pelvic_drop`. For `consequences` and `correction`, the returned value can currently be an empty string, a string, or a list of strings depending on the analysis result and metric.
For **side_view**, `total_score` is not a simple average of the individual segment scores. It is calculated with weighted segment contributions.
# Get calculated data
Source: https://docs.ochy.io/api-reference/analysis/get-calculated-data
/openapi.yaml get /analysis/{analysis_id}/results/calculated_data
Retrieve the calculated biomechanical metrics for a completed analysis.
Only available when the analysis status is `success`.
**Side view analysis** returns:
- Running metrics (speed, step length, pace, step frequency, ground contact time, time of flight, duty factor)
- Overstride analysis (mean angle and score)
- Foot landing patterns per step (footstrike pattern, landing leg, frame timestamps)
- Per-frame joint angles (ankle, knee, elbow, head, trunk) and center of mass
- Foot strike summary angles (arm, head, trunk, front/back leg mean angles)
**Back view analysis** returns:
- Gait cycle stance phase means per side (pronation, knee adduction, pelvic drop)
- Step-by-step landing data (frame timestamps, landing leg, start/end frame IDs)
# Get raw data
Source: https://docs.ochy.io/api-reference/analysis/get-raw-data
/openapi.yaml get /analysis/{analysis_id}/results/raw_data
Retrieve frame-by-frame position data for a completed analysis.
Only available when the analysis status is `success`.
**Side view analysis** returns positions for: head top, nose, eyes, ears, shoulders, elbows, wrists,
hips, knees, ankles, heels, feet, and neck, plus video metadata and human bounding box per frame.
**Back view analysis** returns positions for: C7 (spine), hips (LHIP/RHIP), PSIS landmarks, knees,
calves, ankles, heels (LBHEEL/RBHEEL, LTHEEL/RTHEEL), and metatarsals (LFM1/LFM5, RFM1/RFM5),
plus video metadata and human detection confidence scores per joint.
# List analyses
Source: https://docs.ochy.io/api-reference/analysis/list-analyses
/openapi.yaml get /analysis
Retrieve all analyses belonging to the authenticated user, including their IDs, types,
and current status.
This is the primary endpoint. `GET /analysis/user` is a legacy alias that returns the same data.
# List analyses for a subject
Source: https://docs.ochy.io/api-reference/analysis/list-analyses-for-a-subject
/openapi.yaml get /analysis/subjects/{subject_id}
Retrieve all analysis IDs and their statuses for a specific subject.
The subject must belong to the authenticated partner and must not be deleted.
# Start analysis
Source: https://docs.ochy.io/api-reference/analysis/start-analysis
/openapi.yaml post /analysis/start
Upload a video file and start a biomechanical analysis. Returns a unique `video_id`
that you use to poll for results.
**Video requirements:**
- Maximum file size: 30 MB
- Maximum duration: 10 seconds
- Accepted formats: MP4, MOV, AVI, WEBM
See [example videos](/guides/example-videos) for sample input and output recordings.
**Analysis types:**
- `side_view` -- Running gait from the side. Produces biomechanical scores, metrics, joint angles, foot landing, and running style.
- `back_view` -- Running gait from behind. Produces pronation/supination, pelvic drop, knee adduction, and gait cycle data.
**Link to a subject:**
Pass an optional `subject_id` to associate this analysis with a runner profile.
You can then retrieve all analyses for that subject via
`GET /analysis/subjects/{subject_id}`.
See [subject management](/guides/subject-management) for the full workflow.
**Webhooks (optional):** Set `notify_via_webhook=true` to receive progress and completion
events on the HTTPS URL you registered with [`PUT /webhook`](/api-reference/webhooks/configure-webhook).
Defaults to `false`. When `true` but no valid, enabled webhook is configured, the analysis still starts, with notifications disabled.
See [API keys and webhooks](/guides/webhooks-and-api-keys) for event types and payload format.
One analysis credit is consumed per request.
**Safe retries:** send an `Idempotency-Key` header to retry this request without creating a
duplicate analysis or consuming another credit if you did not receive the response. See
[limits and reliability](/guides/limits-and-reliability#safe-retries-with-idempotency-key).
**Limits:** 100 requests per minute per API key and 500 requests per minute per IP, applied
as throttling. There is no per-account cap on concurrent or pending analyses. See
[rate limits](/guides/limits-and-reliability#rate-limits).
# Search exercises by lookup keys
Source: https://docs.ochy.io/api-reference/exercises/search-exercises-by-lookup-keys
/openapi.yaml post /exercises
Returns exercises whose `lookup_keys` contain at least one of the requested
`search_keys`. Use the same values as **`search_key`** in analysis results
([Search key mapping](/guides/search-key-mapping)).
# Get low-credit alert settings
Source: https://docs.ochy.io/api-reference/settings/get-low-credit-alerts
/openapi.yaml get /settings/low-credit-alerts
Returns the low-credit alert configuration for the credit scope of your API key
(personal or organization). When the scope has unlimited credits, alerts do not apply:
the response has `applicable: false` and `reason: unlimited`.
See [low-credit alerts](/guides/webhooks-and-api-keys#low-credit-alerts) for how alerts
are delivered.
# Update low-credit alert settings
Source: https://docs.ochy.io/api-reference/settings/update-low-credit-alerts
/openapi.yaml put /settings/low-credit-alerts
Replaces the low-credit alert settings for the credit scope of your API key. All fields
are written on every call: omitting `webhookUrl` removes a previously saved URL.
When `enabled` is `true` and an analysis brings your balance to or below `threshold`,
Ochy emails the user who saved the settings and, if `webhookUrl` is set, sends it a
`credits.low` event. The alert fires once per crossing, at most once per hour, and rearms
when the balance rises above the threshold. A `threshold` of `0` never fires.
# Get shoe recommendation results
Source: https://docs.ochy.io/api-reference/shoe-recommendation/get-shoe-recommendation-results
/openapi.yaml get /shoeRecommendation/{shoe_recommendation_id}/result
**Step 3 of 3** -- Get shoe recommendation results for a recommendation started in step 2.
Returns **200** with the result payload: `recommendations`, `best_matching_shoe`, `all_matches`, and `user_id`. Catalog shoes include top-level `gtins` and `mpns` (string arrays, possibly empty).
# Start shoe recommendation processing
Source: https://docs.ochy.io/api-reference/shoe-recommendation/start-shoe-recommendation-processing
/openapi.yaml post /shoeRecommendation/start
**Step 2 of 3** -- Trigger shoe recommendation processing for a recommendation created in step 1.
Returns `202` immediately; use the result endpoint to poll for completion.
# Submit shoe recommendation questionnaire
Source: https://docs.ochy.io/api-reference/shoe-recommendation/submit-shoe-recommendation-questionnaire
/openapi.yaml post /shoeRecommendation/form
**Step 1 of 3** -- Create a shoe recommendation request.
Send the runner questionnaire and references to completed analyses. The server returns a
`shoe_recommendation_id` that you pass to `POST /shoeRecommendation/start`, then poll
`GET /shoeRecommendation/{shoe_recommendation_id}/result` until the status is `success`.
**Subject link:** When you set `input.form.subject_id`, it must be a **subject id** returned by
`POST /subjects` (same id you may pass as `subject_id` on `POST /analysis/start`). If you
omit `subject_id`, no subject lookup is performed for that request.
**Analyses:** At least one of `id_side_view_analysis` or `id_back_view_analysis` is required.
Each id must be a `video_id` from a completed analysis (`GET /analysis/{analysis_id}/results`
with `status: success`) that belongs to your account. Side-view ids must refer to a
`side_view` analysis; back-view ids to a `back_view` analysis. If you send `subject_id` and an
analysis was created with a `subject_id`, that id must match `subject_id`.
**Height and weight:** Height is in **meters** (defaults to 1.70 if omitted). Weight is in
**kilograms** (defaults to 70 if omitted).
**Shoe purposes:** `tempo_training` is accepted; the alias `tempo` is also accepted and stored
as `tempo_training`.
# Create subject
Source: https://docs.ochy.io/api-reference/subjects/create-subject
/openapi.yaml post /subjects
Create a new subject. The `subject_id` is generated by the server.
All body fields are optional -- you can create a subject with no data and update it later.
**Metric system:** `height` is in **meters** and `weight` is in **kilograms**.
# Delete subject
Source: https://docs.ochy.io/api-reference/subjects/delete-subject
/openapi.yaml post /subjects/{subject_id}/delete
Soft-delete a subject. The record is kept but marked as deleted
and excluded from list results. Uses `POST` instead of `DELETE` to prevent
accidental deletion.
# Get subject
Source: https://docs.ochy.io/api-reference/subjects/get-subject
/openapi.yaml get /subjects/{subject_id}
Retrieve a single subject by ID. Returns 404 if the subject does not exist
or has been soft-deleted.
# List subjects
Source: https://docs.ochy.io/api-reference/subjects/list-subjects
/openapi.yaml get /subjects
Retrieve all subjects belonging to the authenticated partner.
Soft-deleted subjects are excluded.
# Update subject
Source: https://docs.ochy.io/api-reference/subjects/update-subject
/openapi.yaml put /subjects/{subject_id}
Update an existing subject. Only the fields provided in the request body
are updated -- omitted fields remain unchanged.
# Configure partner webhook
Source: https://docs.ochy.io/api-reference/webhooks/configure-webhook
/openapi.yaml put /webhook
Create or update the HTTPS URL where Ochy sends analysis progress and completion events.
You can also enable optional HMAC-SHA256 signatures for webhook delivery.
- If no webhook exists yet, this **creates** the configuration.
- If a webhook already exists, this **updates** the URL and/or the `enabled` flag.
- If `signature.enabled` is `true`, Ochy signs webhook requests with HMAC-SHA256.
Set `enabled` to `false` to stop all webhook traffic until you enable it again.
To receive events for a given analysis, you must also pass `notify_via_webhook=true` on
[`POST /analysis/start`](/api-reference/analysis/start-analysis). See [API keys and webhooks](/guides/webhooks-and-api-keys)
for event types, payload shape, and ordering (`sequence`).
# Get partner webhook configuration
Source: https://docs.ochy.io/api-reference/webhooks/get-webhook
/openapi.yaml get /webhook
Returns the stored webhook URL, `enabled` flag, signature metadata, and `updated_at` for
the authenticated partner. If no configuration has been saved yet, the response body is
empty (`{}`).
Use this to verify settings before relying on [`PUT /webhook`](/api-reference/webhooks/configure-webhook)
or starting analyses with `notify_via_webhook=true`.
# Rotate webhook signing secret
Source: https://docs.ochy.io/api-reference/webhooks/rotate-webhook-signature
/openapi.yaml post /webhook/signature/rotate
Generate a new HMAC-SHA256 signing secret for webhook delivery.
The new `secret` is returned once in this response. Store it securely and update your
webhook receiver to verify future requests with this value.
# Authentication
Source: https://docs.ochy.io/authentication
How to authenticate with the Ochy Partner API using your API key.
Every request to the Partner API requires your API key. Pass it in an **HTTP header** —
either `Authorization: Bearer` (recommended) or `X-API-Key`.
## Passing your API key
### Authorization header (recommended)
Send your key as a Bearer token:
```bash theme={null}
curl "https://partner-api.ochy-prod.com/analysis" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### X-API-Key header (alternative)
If your stack does not manage the `Authorization` header, use `X-API-Key` instead:
```bash theme={null}
curl "https://partner-api.ochy-prod.com/analysis" \
-H "X-API-Key: YOUR_API_KEY"
```
Both headers are equivalent. If both are present, `Authorization` takes precedence.
Do not expose your API key in client-side code. Always call the Ochy API from
your backend server.
## Getting an API key
API keys are generated in the [Ochy developer portal](https://developers.ochy.io). Two paths,
depending on your account type:
* **Self-serve developers:** register at
[developers.ochy.io/register](https://developers.ochy.io/register), verify your email (this
grants 1 free analysis credit), then generate your key from the portal. Key generation requires
a verified email. See [get API access](/get-api-access) for the full walkthrough.
* **Organizations:** once your organization is set up by the Ochy team, an organization admin
signs in to the portal and [generates the key](/guides/webhooks-and-api-keys#generate-a-key).
## Error responses
If authentication fails, the API returns one of these responses:
### Missing API key -- 401
Returned when no API key is provided in the `Authorization` header or the `X-API-Key`
header.
```json theme={null}
{
"detail": "API Key is missing."
}
```
### Invalid API key -- 403
Returned when the key does not match any active partner account.
```json theme={null}
{
"detail": "Invalid API Key"
}
```
## Rate limits
Requests are limited to **100 per minute per API key** and **500 per minute per IP address**. The
limit is a throttle, not a ban: once you slow down, requests are accepted again. Exceeding it
may return `429 Too Many Requests`. See [limits and reliability](/guides/limits-and-reliability)
for polling budgets and retry guidelines.
## Credits
Each analysis consumes one credit from your account balance. The credit is debited
atomically when the analysis starts, not when results are retrieved. If starting the
analysis then fails on our side, the credit is automatically refunded.
### Scopes
Credits belong to the scope of your API key:
* **Personal keys** (self-serve developer accounts) draw from your individual balance.
* **Organization keys** draw from the organization's shared balance.
Your current balance and usage are shown in the **Usage & Credits** tab of the
[developer portal](https://developers.ochy.io).
### Insufficient credits -- 424
Returned when your account has no remaining credits at the time the analysis is started.
```json theme={null}
{
"detail": "Insufficient API credits to perform analysis",
"code": "insufficient_api_credits"
}
```
To add credits: self-serve developers buy them directly in the
[developer portal](https://developers.ochy.io) (2.00 € per analysis, prepaid, see
[get API access](/get-api-access#pricing)); organizations go through their Ochy contact.
### Low-credit alerts
Set a threshold in the portal **Settings** tab or with
[`PUT /settings/low-credit-alerts`](/api-reference/settings/update-low-credit-alerts) to be warned
by email, and optionally by a `credits.low` webhook event, when an analysis brings your balance to
or below it. See [low-credit alerts](/guides/webhooks-and-api-keys#low-credit-alerts).
## Security best practices
* Always pass the key in a header (`Authorization: Bearer` or `X-API-Key`).
* Store your API key in environment variables or a secrets manager -- never hard-code it.
* [Rotate your key](/guides/webhooks-and-api-keys#rotate-a-key) periodically from the developer portal.
* Monitor your credit usage and set up [low-credit alerts](/guides/webhooks-and-api-keys#low-credit-alerts)
to avoid unexpected exhaustion.
# Get API access
Source: https://docs.ochy.io/get-api-access
Create a self-serve developer account, verify your email for a free analysis credit, generate an API key, and buy prepaid credits — no sales contact needed.
Everything related to your API account lives in the
[Ochy developer portal](https://developers.ochy.io): API keys, webhooks, credits, usage,
analyses, and settings. There are two kinds of accounts:
* **Self-serve developer account** — register online and pay per analysis. For individual
developers, indie products, and AI agents. Covered in the first part of this page.
* **Organization account** — set up by the Ochy team with contract pricing and volume credits.
See [organizations](#organizations).
## Self-serve in five steps
The whole path from zero to a working paid integration is self-serve. Card entry at checkout is
the only step that requires a human.
Create an account at
[developers.ochy.io/register](https://developers.ochy.io/register) with email and password,
or continue with Google or Apple.
Already have an Ochy account (mobile app or web app)? Sign in with it instead: the portal asks
you to **enable API access** once, then continues with the steps below.
Ochy sends a verification link when the account is created. Verifying does two things: it
unlocks API key generation, and it grants **1 free analysis credit** (once per account) so you
can run your first analysis without paying.
Open the **Get started** checklist or the **Integration** tab and click **Generate API Key**.
The key is shown **once** — store it in an environment variable or a secrets manager. See
[authentication](/authentication) for how to pass it on requests.
Follow the [quickstart](/quickstart). The first analysis consumes the free credit, so no card
is required to validate your integration.
In the **Usage & Credits** tab (or the **Get started** checklist), choose a quantity (1–500)
and pay through Stripe Checkout. One credit runs one analysis.
## Pricing
| | |
| - | - |
| Price | **2.00 € per analysis** |
| First analysis | Free, granted on email verification |
| Model | Prepaid credits — no subscription |
| Payment | Stripe Checkout (card), 1–500 credits per purchase. Applicable taxes are calculated at checkout. |
| Invoices | An invoice is issued for every purchase, downloadable from **Billing history & invoices** in the portal |
Each analysis debits one credit when it starts. If starting the analysis then fails on our side,
the credit is refunded automatically. When your balance reaches zero, `POST /analysis/start`
returns **424** with code `insufficient_api_credits`: buy more credits in the portal to continue.
To be warned before that happens, set up [low-credit alerts](/guides/webhooks-and-api-keys#low-credit-alerts).
API keys are free. You only pay for analyses, so you can integrate and test request handling
before buying anything.
## For AI agents
An AI agent can complete every step of this funnel for its user except card entry:
1. **Register** the account at `https://developers.ochy.io/register` (email and password form).
2. **Ask the user to click the verification link** in their inbox. The free analysis credit is
granted once the email is verified.
3. **Generate the API key** from the portal and store it for the user (it is shown once).
4. **Run the first analysis** on the free credit to validate the integration end to end.
5. **Hand off payment**: start the credit purchase in the portal and give the Stripe Checkout URL
to the user. Card entry is intentionally a human step.
Machine-readable entry points:
| File | URL |
| - | - |
| Portal funnel description | `https://developers.ochy.io/llms.txt` |
| Documentation index | `https://docs.ochy.io/llms.txt` |
| Full documentation text | `https://docs.ochy.io/llms-full.txt` |
Every documentation page is also available as Markdown by appending `.md` to its URL.
## Organizations
Organization accounts are created by the Ochy team. Once your organization is set up:
* **Organization admins** sign in to [developers.ochy.io](https://developers.ochy.io) with their
Ochy account. API keys are organization-scoped and draw from the organization's shared credit
balance. Members without the admin role cannot open the portal.
* If your organization requires **email verification codes** at sign-in, the portal asks for the
code sent by email before the dashboard loads (email and password sign-in only).
* Credits are provisioned by contract (monthly allocations or unlimited plans). Online purchase is
for self-serve accounts only: top-ups for organizations go through your Ochy contact.
* Organization admins also get a **Shoes** tab to manage the catalog used by
[shoe recommendation](/guides/shoe-recommendation#shoe-catalog).
The API section of the Ochy web app (`app.ochy.io/organization/api`) has moved to the developer
portal. [Contact sales](https://www.ochy.io) to set up an organization.
# Analysis workflow
Source: https://docs.ochy.io/guides/analysis-workflow
Understand the full lifecycle of a video analysis -- from upload to retrieving results.
This guide walks through the complete analysis lifecycle: uploading a video, monitoring
progress, and retrieving different levels of result data.
## Video requirements
Before uploading, make sure your video meets these constraints:
| Constraint | Requirement |
| - | - |
| File size | 30 MB maximum |
| Duration | 10 seconds maximum |
| Format | MP4, MOV, AVI, or WEBM |
| Resolution | 1080p recommended (max 1080p height) |
| Frame rate | 30 fps minimum, 60 fps recommended |
### Orientation
Required orientation depends on the analysis type and running condition (see the `treadmill`
parameter below). Submitting a video with the wrong orientation will produce poor results or
cause the analysis to fail.
See [example videos](/guides/example-videos) for the full orientation matrix, typical
resolutions, and reference input/output clips per scenario.
## Analysis types
Choose the analysis type based on the camera position:
Camera is positioned to the side of the runner. Set `analysis_type=side_view`.
**Produces:**
* Overall biomechanical score (0 -- 1)
* Running metrics: speed, step length, cadence, ground contact time, time of flight, duty factor
* Overstride analysis with angle and score
* Foot landing classification per step (rearfoot, midfoot, forefoot)
* Joint angles: ankle, knee, hip, shoulder, elbow
* Running style classification with confidence percentage
Camera is positioned behind the runner. Set `analysis_type=back_view`.
**Produces:**
* Gait cycle analysis with stance phase means per leg
* Pronation / supination per foot
* Pelvic drop
* Knee adduction
* Step-by-step landing data with frame timestamps
See [example videos](/guides/example-videos) for sample input and output videos of each analysis type.
## Parameters
| Parameter | Type | Required | Default | Description |
| - | - | - | - | - |
| `video` | file | Yes | -- | Video file to analyze |
| `analysis_type` | string | Yes | `side_view` | `side_view` or `back_view` |
| `height` | number | No | 1.70 | Runner height in meters |
| `weight` | number | No | 70 | Runner weight in kilograms |
| `pace` | string | Conditional | -- | Running pace `MM:SS`. Required when `treadmill=true`. Auto-calculated for overground running. |
| `language` | string | No | `en` | Result language: `en`, `fr`, `de`, `zh`, `ko`, `pt`, `es`, `it`, `ja` |
| `treadmill` | boolean | No | `false` | Whether the runner is on a treadmill |
| `subject_id` | string | No | -- | Subject ID to link the analysis to. See [subject management](/guides/subject-management). |
## Lifecycle
```mermaid theme={null}
sequenceDiagram
participant Client
participant API as Ochy API
Client->>API: POST /analysis/start (video + params)
API-->>Client: 202 { video_id }
loop Poll every 3-5 seconds
Client->>API: GET /analysis/{id}/results
alt pending
API-->>Client: 200 { status: pending, percentage: 0 }
else analyzing
API-->>Client: 202 { status: analyzing, percentage: 45 }
else success
API-->>Client: 200 { status: success, biomechanical_analysis, ... }
else failed
API-->>Client: 400 { status: failed }
end
end
Client->>API: GET /analysis/{id}/results/calculated_data
API-->>Client: 200 { metrics, overstride, foot_landing, ... }
Client->>API: GET /analysis/{id}/results/raw_data
API-->>Client: 200 { frames, video_input_information }
```
### Status progression
| Status | HTTP code | Meaning |
| - | - | - |
| `pending` | 200 | Analysis is queued, waiting for a processing slot. |
| `analyzing` | 202 | Processing is underway. The `percentage` field shows progress. |
| `success` | 200 | Analysis is complete. Full results are available. |
| `failed` | 400 | Analysis could not be completed. No results available. |
### Polling strategy
Poll the results endpoint every 3 -- 5 seconds. Most analyses complete within
10 -- 30 seconds depending on video length and server load.
Requests are limited to 100 per minute per API key and 500 per minute per IP. Polling one
analysis every 5 seconds uses 12 requests per minute, so one key can poll about 8 analyses in
parallel at that rate. For more concurrent analyses, use [webhooks](/guides/webhooks-and-api-keys)
and a slower fallback poll. See [limits and reliability](/guides/limits-and-reliability#budget-your-polling).
An analysis that stays `pending` or `analyzing` for 30 minutes is automatically marked `failed`
and its credit refunded. There is no per-account cap on concurrent or pending analyses.
```python theme={null}
import time
import requests
def wait_for_analysis(video_id: str, api_key: str, interval: float = 3.0) -> dict:
"""Poll until analysis completes or fails."""
url = f"https://partner-api.ochy-prod.com/analysis/{video_id}/results"
while True:
response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
data = response.json()
if data["status"] in ("success", "failed"):
return data
time.sleep(interval)
```
### Webhooks (optional)
You can register an HTTPS endpoint and enable notifications per analysis to receive progress
and completion events without polling. See [API keys and webhooks](/guides/webhooks-and-api-keys)
## Result levels
Once the status is `success`, you can access three levels of data:
The results endpoint returns everything in a single call: status, signed URLs
(thumbnail and annotated video), and the `biomechanical_analysis` object containing
scores, metrics, and classifications. Each segment's `texts` object includes
`recap`, `consequences`, and `correction` for both side view and back view
(side view may also include `coach_tips`).
Each biomechanical segment includes `score_number`, `measured_value`, and
score ranges such as `range_good`, `range_okay`, and sometimes `range_bad`.
`measured_value` and all range values are expressed in degrees. Range
boundaries can overlap: for example, with `range_good: [0, 102]` and
`range_okay: [102, 111]`, values up to `102` are good and values strictly
greater than `102` are okay.
For side view, `total_score` is a weighted calculation, not a simple average
of the segment scores.
Need to render a gauge or scale? See [measurement scales](/guides/measurement-scales)
for the full min/max boundaries and display recommendations for each segment.
Use this when you need a complete snapshot of the analysis.
Structured numerical metrics without media URLs. Ideal when you need to process
or display specific values.
**Side view** includes: `metrics`, `overstride`, `foot_landing`, `joint_angles`,
`running_styles`, and `total_score`.
**Back view** includes: `gait_cycle` with stance means per leg (pronation,
knee adduction, pelvic drop) and step-by-step data.
Frame-by-frame position data for every anatomical landmark detected. Includes
`video_input_information` (resolution, FPS, duration) and a `frames` object
keyed by frame number.
Use this for custom visualizations, overlays, or your own biomechanical models.
## Error handling
Common errors during the analysis workflow:
| Error | Code | Status | When |
| - | - | - | - |
| File too large | `file_too_large` | 413 | Video exceeds 30 MB |
| Video too long | -- | 400 | Video exceeds 10 seconds |
| Invalid file type | `invalid_file_type` | 400 | Unsupported extension (not MP4, MOV, AVI, or WEBM) |
| Not completed | -- | 400 | Requesting calculated/raw data before `success` |
| Insufficient credits | `insufficient_api_credits` | 424 | No credits remaining |
| Invalid idempotency key | `invalid_idempotency_key` | 400 | `Idempotency-Key` is not 1 to 255 printable ASCII characters |
| Throttled | -- | 429 | More than 100 requests per minute for your key, or 500 per minute for your IP. Retry with backoff. |
| Server error | -- | 500 | Temporary failure on Ochy's side. Retry with backoff. |
| Not found | -- | 404 | Invalid `analysis_id`, or its data retention period has expired |
To retry `POST /analysis/start` safely, send an `Idempotency-Key` header. See
[limits and reliability](/guides/limits-and-reliability).
# Example videos
Source: https://docs.ochy.io/guides/example-videos
Sample input and output videos for each analysis type -- back view and side view, overground and treadmill.
Reference videos showing what to film (input) and what the analysis produces (output) for each combination of analysis type and running condition.
## Video requirements at a glance
Every video sent to the API must meet the general constraints below. **Orientation varies by analysis type** -- see the per-scenario tables further down.
| Constraint | Requirement |
| - | - |
| Format | MP4, MOV, AVI, or WEBM |
| Max duration | 10 seconds |
| Max file size | 30 MB |
| Resolution | 1080p recommended (max 1080p height) |
| Frame rate | 30 fps minimum, 60 fps recommended |
| Framing | Runner clearly visible and occupying a large portion of the frame |
### Orientation by scenario
| Analysis type | Condition | Orientation | Typical resolution |
| - | - | - | - |
| Side view | Overground (`treadmill=false`) | **Landscape** (16:9) | 1920 x 1080 |
| Side view | Treadmill (`treadmill=true`) | **Portrait** (9:16) | 1080 x 1920 |
| Back view | Overground (`treadmill=false`) | **Portrait** (9:16) | 1080 x 1920 |
| Back view | Treadmill (`treadmill=true`) | **Portrait** (9:16) | 1080 x 1920 |
Submitting a video with the wrong orientation for a given analysis type will produce
poor results or cause the analysis to fail. Double-check the table above before uploading.
***
Camera is positioned to the side of the runner. Set `analysis_type=side_view`.
### Overground
`treadmill=false`
| Property | Requirement |
| - | - |
| Orientation | **Landscape** (width > height) |
| Typical resolution | 1920 x 1080 |
| Duration | Up to 10 s |
| Framing | Full body visible from head to feet, runner moving left-to-right or right-to-left |
### Treadmill
`treadmill=true`
| Property | Requirement |
| - | - |
| Orientation | **Portrait** (height > width) |
| Typical resolution | 1080 x 1920 |
| Duration | Up to 10 s |
| Framing | Full body visible from head to feet, camera at the side of the treadmill |
Camera is positioned behind the runner. Set `analysis_type=back_view`.
### Overground
`treadmill=false`
| Property | Requirement |
| - | - |
| Orientation | **Portrait** (height > width) |
| Typical resolution | 1080 x 1920 |
| Duration | Up to 10 s |
| Framing | Full body visible from head to feet, runner moving away from camera |
### Treadmill
`treadmill=true`
| Property | Requirement |
| - | - |
| Orientation | **Portrait** (height > width) |
| Typical resolution | 1080 x 1920 |
| Duration | Up to 10 s |
| Framing | Full body visible from head to feet, camera behind the treadmill |
# Exercises
Source: https://docs.ochy.io/guides/exercises
Search exercise content by lookup keys aligned with analysis search_key values.
Use **`POST /exercises`** to retrieve recommended **exercises** from Ochy's catalog. Each exercise is matched when its **`lookup_keys`** contain at least one of the strings you pass in **`search_keys`**.
The values you send in **`search_keys`** are the same stable identifiers as **`search_key`** in analysis results (`texts.search_key`). See the full catalog of keys and meanings in **[Search key mapping](/guides/search-key-mapping)**.
Map analysis items by **`search_key`**, then call this endpoint with those keys to fetch titles, media, difficulty, series, and duration or repetition ranges—without relying on localized labels from the analysis response.
## Authentication
Same as the rest of the Partner API: pass your API key in the **`Authorization: Bearer`** header (or `X-API-Key`) on every request. See [Authentication](/authentication).
## Request
| Item | Value |
| - | - |
| Method | `POST` |
| Path | `/exercises` |
| Auth | `Authorization: Bearer ` header (required) |
| Body | JSON `application/json` |
### Body schema
| Field | Type | Description |
| - | - | - |
| `search_keys` | `string[]` | At least one lookup key (e.g. values from [Search key mapping](/guides/search-key-mapping)). |
| `language` | `string` | Locale for the exercise `title` (e.g. `en`, `fr`). Falls back to English if the locale is missing. |
### Example body
```json theme={null}
{
"language": "en",
"search_keys": [
"HeadG",
"PelvicB"
]
}
```
## Response
* **200 OK** — JSON **array** of exercise objects. Each object includes **`id`** (UUID), **`lookup_keys`**, and a localized **`title`**.
* **`lookup_keys`** lists the stable identifiers used to match this exercise (same values as **`search_keys`** in the request and **`search_key`** in analysis results).
* **`media`** may include `image`, `video`, and optionally **`video_portrait`**.
### Example response (200)
```json theme={null}
[
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"lookup_keys": ["PelvicB", "HeadG"],
"difficulty": "easy",
"effort_per_rep": {
"max_ms": 15000,
"min_ms": 10000
},
"exercise_format": "duration",
"kind": "exercise",
"media": {
"image": "https://firebasestorage.googleapis.com/v0/b/ochy-7871a.appspot.com/o/assets%2Fexercises%2Fbackview%2Fthumbnails%2FWall%20Assist%20Stork%20Balance.png?alt=media",
"video": "https://firebasestorage.googleapis.com/v0/b/ochy-7871a.appspot.com/o/assets%2Fexercises%2Fbackview%2Fvideos%2FWall-Assisted-Stork-Balance.mp4?alt=media&token=1c016167-d868-429d-9afb-1d2af1373a1a"
},
"number_of_series": 4,
"title": "Wall Assist Stork Balance"
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"lookup_keys": ["PelvicB"],
"difficulty": "medium",
"exercise_format": "repetitions",
"kind": "exercise",
"media": {
"image": "https://firebasestorage.googleapis.com/v0/b/ochy-7871a.appspot.com/o/assets%2Fexercises%2Fbackview%2Fthumbnails%2FStep%20Up%20to%20Balance.png?alt=media&token=c6de01b3-f5f7-4fec-95f2-41f50283b718",
"video": "https://firebasestorage.googleapis.com/v0/b/ochy-7871a.appspot.com/o/assets%2Fexercises%2Fbackview%2Fvideos%2FStep%20Up%20to%20Balance.mp4?alt=media&token=cb8972bf-c8b3-4fcb-89e9-b591c4b4cd5d"
},
"number_of_series": 4,
"repetitions": {
"max": 10,
"min": 6
},
"title": "Step Up to Balance"
}
]
```
A larger catalog match can return many items in one array; the shape is always the same.
## Errors
| HTTP | `code` | When |
| - | - | - |
| 400 | `invalid_body` | Malformed JSON or missing required fields. |
| 400 | `language_required` | `language` is empty. |
| 400 | `search_keys_empty` | No usable values in `search_keys`. |
| 404 | `no_exercises_found` | No exercises matched the given keys. |
### Example — nothing matched (404)
```json theme={null}
{
"code": "no_exercises_found",
"detail": "No exercises found for the given search keys"
}
```
Other errors (e.g. invalid API key, internal failure) follow the same **`detail`** / **`code`** pattern as the rest of the Partner API.
## Example request (cURL)
```bash theme={null}
curl -sS -X POST 'https://partner-api.ochy-prod.com/exercises' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{"language":"en","search_keys":["HeadG","PelvicB"]}'
```
Replace the base URL with your environment (e.g. staging) if needed.
# Limits and reliability
Source: https://docs.ochy.io/guides/limits-and-reliability
Rate limits, concurrency, timeouts, safe retries with Idempotency-Key, and how to recover from lost responses or missed webhooks.
This guide gathers the operational rules to design a reliable integration: how many requests you
can send, what happens when analyses queue up, how to retry safely, and how to recover when a
response or a webhook never reaches you.
## Rate limits
Limits apply to every Partner API endpoint, including `POST /analysis/start` and the polling
endpoint `GET /analysis/{analysis_id}/results`. There is no separate budget for starting analyses
and polling results.
| Scope | Limit |
| - | - |
| Per API key | **100 requests per minute** |
| Per IP address | **500 requests per minute** |
The limits are **throttling, not banning**: when you exceed them, requests are slowed down or
rejected with `429 Too Many Requests` for a short time, then traffic flows again as soon as your
request rate drops back under the limit. Your key is never blocked or revoked because of it.
The IP limit is shared by every key calling from the same address. If several of your services
or stores egress through one NAT or proxy IP, they share the 500 requests per minute.
### Budget your polling
Polling is usually what consumes the 100 requests per minute. Each analysis polled every 5 seconds
uses 12 requests per minute, so a single key can poll roughly 8 analyses in parallel before being
throttled.
If you run several stores or many concurrent analyses on one key:
* Use [webhooks](/guides/webhooks-and-api-keys) as the primary signal and poll only as a fallback,
at a slow interval (for example every 30 seconds for analyses that have not reached a terminal
event).
* Use a **single poller per API key** that schedules all analyses, rather than one poller per
store, so you can enforce your own budget.
* Do not poll `GET /analysis` (the list endpoint) to track progress. It returns every analysis in
one unpaginated response, without timestamps.
* Back off when you receive `429` or `5xx` (see [retry guidelines](#retry-guidelines)).
## Concurrency and pending analyses
There is **no per-account cap** on the number of analyses processed concurrently or waiting in
`pending`. Ochy does not reject a start request because other analyses are in progress: a valid
`POST /analysis/start` is always accepted with `202` and a `video_id`, and waits its turn in
`pending` if needed.
An accepted analysis moves through the statuses described in the
[analysis workflow](/guides/analysis-workflow#status-progression). While capacity is busy it stays
`pending`; it then becomes `analyzing`, and ends as `success` or `failed`.
The only business reasons for a start request to be refused are validation errors (`400`, `413`)
and an empty balance (`424 insufficient_api_credits`).
### How long can an analysis take?
Most analyses complete within 10 to 30 seconds. If an analysis does not reach a terminal state
(`success` or `failed`) within **30 minutes** of being accepted, Ochy marks it as `failed` and
automatically refunds the credit. The check runs hourly, so the `failed` status can appear up to
about 90 minutes after the start.
We recommend investigating an analysis that is still `pending` after about 5 minutes, or whose
`percentage` has not moved for about 5 minutes while `analyzing`. There is no need to resubmit
it: it will either complete or be failed and refunded automatically. Resubmitting creates a new
analysis and consumes another credit.
## Safe retries with `Idempotency-Key`
If you do not receive the response of `POST /analysis/start` (timeout, dropped connection), you
cannot know whether the analysis was created. Send an `Idempotency-Key` header with every start
request so you can retry without creating a duplicate or consuming another credit.
```bash theme={null}
curl -X POST "https://partner-api.ochy-prod.com/analysis/start" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: 6f1c2d7e-0b7a-4c52-9a43-4f7f3d1d2a10" \
-F "video=@runner_side.mp4" \
-F "analysis_type=side_view"
```
| Rule | Detail |
| - | - |
| Format | 1 to 255 printable ASCII characters (a UUID is a good choice). Anything else returns `400` with code `invalid_idempotency_key`. |
| Scope | Per API key scope (personal or organization). |
| Lifetime | **24 hours** after the analysis was accepted. |
| Replay | Retrying with the same key returns `202` with the **original** `video_id` and the response header `Idempotent-Replayed: true`. No new analysis is created and no new credit is debited. |
| Empty balance | A replay still succeeds when your balance has reached zero. |
| Request body | The retry must still include the video. The server does not compare it to the original request: reuse a key only for the same logical submission. |
| Failure on our side | If the analysis could not be handed over for processing, the credit is refunded and the key is released, so a retry with the same key starts a fresh analysis. |
Generate the key **once per logical submission** (for example when you create the job in your own
database), store it with the job, and reuse it for every retry of that job.
## Recovering a lost `video_id`
Send the same request again with the same `Idempotency-Key`. You get back the original
`video_id` whether or not the first request had reached us. This works the same for polling
and webhook integrations: once you hold the `video_id`, you can poll it or match its webhook
events.
If you attached a `subject_id`, [`GET /analysis/subjects/{subject_id}`](/api-reference/analysis/list-analyses-for-a-subject)
lists the analyses linked to that subject with their `id` and `status`.
[`GET /analysis`](/api-reference/analysis/list-analyses) returns `id`, `analysis_type`,
`status` and `subject_id` for every analysis. It has no creation time and no client
reference, so it cannot identify one specific request on its own.
Webhook events carry only the `analysis_id` (the same value as `video_id`). They do not include
any client-side reference. If you never received the `video_id`, you cannot match an incoming
event to a request: recover the `video_id` with an idempotent retry first.
## Webhook delivery guarantees
Each webhook event is delivered **at most once**: Ochy makes a single attempt with a 10-second
timeout. There is **no automatic retry** if your endpoint is down, times out, or answers with a
non-`2xx` status.
This is deliberate. Progress events are only valuable when they arrive on time: a progress update
delivered several seconds late is already outdated. A retry policy would also mean repeatedly
calling an endpoint that may have been misconfigured, for example a wrong URL.
What this means for your integration:
* Treat webhooks as a fast notification, not as a guaranteed log.
* Keep a **fallback poll** for every analysis that has not reached a terminal event
(`analysis.completed` or `analysis.failed`), at a slow interval such as 30 seconds, and within
your [rate limit budget](#budget-your-polling).
* Order and deduplicate with the per-analysis `sequence`. Missing `sequence` numbers are expected;
only the terminal state matters.
* Respond with `2xx` quickly and process the payload in the background.
* Use `GET /analysis/{analysis_id}/results` as the single source of truth after any delivery issue.
## Retry guidelines
| Response | Retry? | What to do |
| - | - | - |
| Network error, timeout, no response | Yes | Retry with the same `Idempotency-Key` for `POST /analysis/start`. |
| `429` | Yes | You are throttled. Wait and retry with exponential backoff. |
| `500`, `502`, `503`, `504` | Yes | Retry with exponential backoff and the same `Idempotency-Key`. |
| `400`, `413` | No | Fix the request (parameters, file type, size, duration, `Idempotency-Key` format). |
| `401`, `403` | No | Fix or rotate your API key. |
| `404` | No | The `analysis_id` is unknown for your key, or its data retention period has expired. |
| `424` | No | Your balance is empty. Add credits, then start again. |
Use exponential backoff with jitter, for example 2 s, 4 s, 8 s, up to a ceiling of about 60 s,
and stop after a bounded number of attempts. Ochy does not send a `Retry-After` header, so rely
on your own backoff.
```python theme={null}
import random
import time
import uuid
import requests
START_URL = "https://partner-api.ochy-prod.com/analysis/start"
RETRYABLE = {429, 500, 502, 503, 504}
def start_analysis(api_key: str, video_path: str, max_attempts: int = 5) -> str:
"""Start an analysis; safe to retry because every attempt reuses the same key."""
idempotency_key = str(uuid.uuid4()) # generate once per submission, persist it with your job
for attempt in range(max_attempts):
try:
with open(video_path, "rb") as video:
response = requests.post(
START_URL,
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": idempotency_key,
},
files={"video": video},
data={"analysis_type": "side_view"},
timeout=120,
)
except requests.RequestException:
response = None
if response is not None and response.status_code == 202:
return response.json()["video_id"]
if response is not None and response.status_code not in RETRYABLE:
response.raise_for_status()
time.sleep(min(60, 2 ** (attempt + 1)) + random.random())
raise RuntimeError("Could not start the analysis")
```
# Measurement scales
Source: https://docs.ochy.io/guides/measurement-scales
Understand the scale boundaries, scoring ranges, and how to display biomechanical gauges for each measurement.
When rendering a gauge or scale for biomechanical measurements, you need to know the
**overall min/max boundaries** of the scale in addition to the `range_good` and `range_okay`
values returned by the API.
This guide documents the fixed scale boundaries for every measurement type so you can
render consistent visualizations regardless of the runner's measured value.
## How scoring ranges work
Each biomechanical measurement in the API response includes:
| Field | Description |
| - | - |
| `measured_value` | The actual measured angle (in degrees) |
| `score_number` | Normalized score between 0 and 1 |
| `range_good` | `[min, max]` — values within this range score highest |
| `range_okay` | `[min, max]` — values within this range score moderately |
| `range_bad` | `[min, max]` — (back view only) values outside okay, scoring lowest |
All range values are expressed in **degrees**. Range boundaries can overlap — see
[analysis workflow](/guides/analysis-workflow) for inclusion/exclusion rules.
## Back view — fixed scale boundaries
Back-view measurements use **fixed ranges** that do not change based on athlete morphology.
The API returns `range_bad` for back-view segments — use it as the overall scale boundary.
### Pelvic drop (per side)
| Field | Value | Unit |
| - | - | - |
| `range_good` | `[0, 2]` | degrees |
| `range_okay` | `[2, 4]` | degrees |
| `range_bad` | `[4, 8]` | degrees |
| **Scale min** | **0** | degrees |
| **Scale max** | **8** | degrees |
`range_bad[1]` (8°) is the scale maximum. Display the gauge from 0 to 8.
### Pronation / supination (per side)
| Field | Value | Unit |
| - | - | - |
| `range_good` | `[-5, 5]` | degrees |
| `range_okay` | `[-10, 10]` | degrees |
| `range_bad` | `[-20, 20]` | degrees |
| **Scale min** | **-20** | degrees |
| **Scale max** | **20** | degrees |
Negative values indicate pronation (foot rolling inward), positive values indicate
supination (foot rolling outward).
### Knee adduction (per side)
| Field | Value | Unit |
| - | - | - |
| `range_good` | `[-3, 3]` | degrees |
| `range_okay` | `[-5, 5]` | degrees |
| `range_bad` | `[-10, 10]` | degrees |
| **Scale min** | **-10** | degrees |
| **Scale max** | **10** | degrees |
Negative values indicate adduction (knee inward), positive values indicate abduction
(knee outward).
### Back view summary
```
Pelvic drop: |====GOOD====|==OKAY==|=====BAD=====|
0 2 4 8
Pronation: |===BAD===|==OK==|===GOOD===|==OK==|===BAD===|
-20 -10 -5 5 10 20
Knee add.: |===BAD===|=OK=|==GOOD==|=OK=|===BAD===|
-10 -5 -3 3 5 10
```
## Side view — personalized scale boundaries
Side-view ranges are **personalized** based on the runner's height, weight, and pace.
The `range_good` and `range_okay` values returned by the API vary between analyses.
The API does not return `range_bad` for side-view measurements. Use the recommended
display ranges below to render consistent gauges.
### Recommended display ranges
Use these fixed boundaries when rendering side-view gauges. They accommodate the full
range of personalized values the API can return:
| Measurement | Scale min | Scale max | Unit | Notes |
| - | - | - | - | - |
| **Head** | `range_okay[0] - 10` | `range_okay[1] + 10` | degrees | Negative = looking down, positive = looking up |
| **Trunk (back)** | `range_okay[0] - 10` | `range_okay[1] + 10` | degrees | Forward lean angle |
| **Arm** | `range_okay[0] - 10` | `range_okay[1] + 10` | degrees | Elbow angle |
| **Front knee** | `range_okay[0] - 10` | `180` | degrees | Knee extension at foot landing |
| **Back knee** | `0` | `range_okay[1] + 10` | degrees | Knee flexion (heel kick) |
| **Foot (overstride)** | `-10` | `20` | degrees / % | Overstride angle relative to center of gravity |
The formula `range_okay[0] - 10` / `range_okay[1] + 10` provides a safe margin around
any measured value while keeping the scale readable.
### Foot / overstride (fixed)
Unlike other side-view segments, foot/overstride uses **fixed ranges** regardless of
athlete morphology:
| Field | Value |
| - | - |
| `range_good` | `[-10, 5]` |
| `range_okay` | `[5, 10]` |
| **Scale min** | **-10** |
| **Scale max** | **20** |
| Field | Value |
| - | - |
| `range_good` | `[-10, 10]` |
| `range_okay` | `[10, 20]` |
| **Scale min** | **-10** |
| **Scale max** | **30** |
Negative values mean the foot lands behind the center of gravity (good). Positive values
indicate overstride (foot landing ahead of the center of gravity).
### Side view — typical range examples
These are examples of actual API responses for a 1.75m / 70kg runner at 5:00 min/km pace:
| Measurement | `range_good` | `range_okay` | Recommended scale |
| - | - | - | - |
| Head | `[-14, -4]` | `[-18, 0]` | `[-28, 10]` |
| Trunk | `[6, 12]` | `[2, 16]` | `[-8, 26]` |
| Arm | `[75, 85]` | `[71, 89]` | `[61, 99]` |
| Front knee | `[145, 180]` | `[136, 145]` | `[126, 180]` |
| Back knee | `[0, 97]` | `[97, 106]` | `[0, 116]` |
| Foot | `[-10, 5]` | `[5, 10]` | `[-10, 20]` |
Side-view ranges change with athlete morphology. The values above are illustrative —
always compute your scale boundaries from the actual `range_okay` values returned per analysis.
## Implementation example
```javascript theme={null}
function getScaleBounds(segment, analysisType) {
// Back view: use range_bad directly
if (analysisType === 'back_view') {
return {
min: segment.range_bad[0],
max: segment.range_bad[1],
};
}
// Side view foot: fixed scale
if (segment.texts?.search_key?.startsWith('Midfoot') ||
segment.texts?.search_key?.startsWith('Rearfoot') ||
segment.texts?.search_key?.startsWith('Forefoot')) {
return { min: -10, max: 20 };
}
// Side view forward_cycle: lower bound from range_okay, upper bound is 180
if (segment.texts?.title === 'Front knee angle at foot landing') {
return {
min: segment.range_okay[0] - 10,
max: 180,
};
}
// Side view back_cycle: lower bound is 0, upper bound from range_okay
if (segment.texts?.title === 'Back knee angle at foot landing') {
return {
min: 0,
max: segment.range_okay[1] + 10,
};
}
// Side view head, trunk, arm: ±10 from range_okay
return {
min: segment.range_okay[0] - 10,
max: segment.range_okay[1] + 10,
};
}
```
## Score interpretation
The `score_number` field (0 to 1) indicates how well the measured value falls within
the ideal range:
| Score range | Interpretation |
| - | - |
| 0.7 — 1.0 | Good — within or very near `range_good` |
| 0.4 — 0.7 | Okay — within `range_okay` |
| 0.0 — 0.4 | Needs improvement — outside `range_okay` |
The overall `total_score` for side view is a **weighted** combination of segment scores,
not a simple average.
# Search key mapping
Source: https://docs.ochy.io/guides/search-key-mapping
Map search_key values from analysis results to positions and explanations for internal use.
The `GET /analysis/{analysis_id}/results` response includes a **`search_key`** in each biomechanical item (under `texts`). Use this stable key for internal mapping (e.g. to your own labels, content, or translations) instead of relying on the human-readable `position` or `title`, which may be localized.
The tables below list all possible `search_key` values per analysis type, with the associated **position** and a short **explanation** (from the English content assets).
***
## Back view
Back view analysis returns `analysis_content` with **knee\_adduction**, **pelvic\_drop**, and **pronation**. Each side (`left` / `right`) has a `texts.search_key`.
### Pelvic drop
| search\_key | position | explanation |
| - | - | - |
| `PelvicG` | Pelvis aligned | Right and left sides of pelvis are at the same height; ideal position. |
| `PelvicO` | Mild pelvic drop | Right and left sides of the pelvis are not aligned; strengthen gluteal muscles, hamstrings, adductors and hip flexors. |
| `PelvicB` | Excessive pelvic drop | Right and left sides of the pelvis are not aligned; same strengthening and awareness as PelvicO with higher severity. |
### Foot posture (pronation)
| search\_key | position | explanation |
| - | - | - |
| `Overpronation` | Overpronation | Abnormal pronation; foot moves inward; risk of injuries (e.g. shin splints, plantar fasciitis). |
| `Pronation` | Pronation | Foot moves inward; natural during contact; if excessive, risk of pain and need for proprioception work. |
| `Neutral` | Neutral | Foot straight, no pronation or supination; good position, can reinforce with proprioception. |
| `Supination` | Supination | Foot moves outward; natural when leaving the ground; excess can cause IT band, Achilles, foot issues. |
| `Oversupination` | Oversupination | Abnormal supination; risk of leg injuries, increased stiffness and impact. |
### Knee adduction
| search\_key | position | explanation |
| - | - | - |
| `ExcessiveAbduction` | Excessive abduction | Tibia away from midline, knee collapses inward; strengthen adductors, glutes, quadriceps; linked to knee valgus. |
| `MildAbduction` | Mild abduction | Same direction as excessive but milder; knee inward relative to hip–ankle line. |
| `KneeAligned` | Knee aligned | Tibial movement parallel to midline; ideal knee position and efficiency. |
| `MildAdduction` | Mild adduction | Tibia toward midline, knee shifts outward; strengthen hip, gluteal, abdominal muscles; can be linked to knee varus. |
| `ExcessiveAdduction` | Excessive adduction | Same as mild adduction with higher severity; linked to knee varus. |
***
## Side view
Side view analysis returns `analysis_content` with **arm**, **back\_cycle**, **foot**, **forward\_cycle**, **head**, and **trunk**.
### Arms position
| search\_key | position | explanation |
| - | - | - |
| `ArmBD` | Over-extension | Elbow too open; loss of efficiency; maintain appropriate amplitude. |
| `ArmBT` | Excessive flexion | Elbow too closed; risk of muscle tension; loosen fists and relax shoulders. |
| `ArmG` | Ideal arm position | Good body stability and elbow angle; keep working on this position. |
| `ArmOD` | Moderate extension | Elbow too open; strain on back muscles; slightly bend the elbows. |
| `ArmOT` | Moderate flexion | Elbow too closed; increase arm swinging; relax shoulders and loosen fists. |
### Back knee angle at foot landing
| search\_key | position | explanation |
| - | - | - |
| `BackCycleB` | Low heel kick | Restricted leg cycle; need more energy to maintain speed; sprint exercises to improve amplitude. |
| `BackCycleG` | Ideal Position | Good spring effect of the leg; stride naturally wider; back knee angle is ideal. |
| `BackCycleO` | Okay heel kick | Limited propulsion; back knee too open; work on amplitude of hips and leg. |
### Front knee angle at foot landing
| search\_key | position | explanation |
| - | - | - |
| `ForwardCycleB` | Excessive flexion | Front knee too closed; limited running efficiency; improve leg lift (e.g. skipping). |
| `ForwardCycleG` | Ideal Position | Front knee angle is ideal; optimal running efficiency. |
| `ForwardCycleO` | Moderate flexion | Front knee too closed; work on amplitude (e.g. stair climbing). |
### Foot landing
Side view `analysis_content.foot` includes `texts.search_key`, plus `foot_strike_pattern` (`rearfoot` / `midfoot` / `forefoot`) and overstride-related text fields (`overstride_title`, `overstride_explanation`, `foot_strike_explanation`, `phases`).
Each `search_key` encodes **both** the foot strike pattern and how far ahead of the body the foot lands. Use `texts.search_key` with [POST /exercises](/guides/exercises) for strengthening recommendations tied to that state.
#### Under the body (ideal)
| search\_key | position | overstride\_title |
| - | - | - |
| `RearfootGZ` | Under center of mass | Heel strike under center of mass |
| `MidfootGZ` | Under center of mass | Midfoot strike under center of mass |
| `ForefootGZ` | Under center of mass | Forefoot strike under center of mass |
#### Slight overstride
| search\_key | position | overstride\_title |
| - | - | - |
| `RearfootG` | Slightly overstriding | Heel strike with slight overstride |
| `MidfootG` | Slightly overstriding | Midfoot strike with slight overstride |
| `ForefootG` | Slightly overstriding | Forefoot strike with slight overstride |
#### Moderate overstride
| search\_key | position | overstride\_title |
| - | - | - |
| `RearfootO` | Moderate overstride | Heel strike with moderate overstride |
| `MidfootO` | Moderate overstride | Midfoot strike with moderate overstride |
| `ForefootO` | Moderate overstride | Forefoot strike with moderate overstride |
#### Excessive overstride
| search\_key | position | overstride\_title |
| - | - | - |
| `RearfootB` | Excessive overstride | Heel strike with excessive overstride |
| `MidfootB` | Excessive overstride | Midfoot strike with excessive overstride |
| `ForefootB` | Excessive overstride | Forefoot strike with excessive overstride |
#### Related exercise lookup keys (foot strike only)
The exercises catalog also includes `FORE_FOOT`, `MID_FOOT`, and `REAR_FOOT`. These are **not** returned as `texts.search_key`. They match stride-drill content by foot strike pattern alone. Map them from `foot.foot_strike_pattern`:
| foot\_strike\_pattern | exercise lookup key |
| - | - |
| `forefoot` | `FORE_FOOT` |
| `midfoot` | `MID_FOOT` |
| `rearfoot` | `REAR_FOOT` |
### Head position
| search\_key | position | explanation |
| - | - | - |
| `HeadBD` | Gaze too focused toward the ground | Gaze too low; lift gaze to improve posture; risk of slouched posture. |
| `HeadBT` | Gaze too much skyward | Gaze too high; align with horizon; risk of cervical pain. |
| `HeadG` | Gaze in ideal position | Good body balance; maintain this position. |
| `HeadOD` | Gaze slightly toward the ground | Gaze too low; overuse of neck muscles; lift your gaze. |
| `HeadOT` | Gaze slightly skyward | Gaze too high; align head, neck, and back; risk of muscle tension. |
### Back position (trunk)
| search\_key | position | explanation |
| - | - | - |
| `TrunkBD` | The torso is too leaned forward. | Excessive tension in lumbar muscles; lift gaze to raise the torso. |
| `TrunkBT` | Not leaning forward enough | Torso too leaned back; overload on lumbar and gluteal muscles; brace with neutral pelvis. |
| `TrunkG` | The torso is in an ideal position. | Stride balance favored; stay braced. |
| `TrunkOD` | The torso is slightly leaned forward. | Slightly too far forward; straighten the torso. |
| `TrunkOT` | Not sufficiently forward-leaning | Torso too far back; engage abdominals to stabilize the back. |
***
## Using search\_key in your integration
* Prefer **`search_key`** for logic, routing, or mapping (e.g. to your own copy or assets).
* Use **`position`** and **`title`** for display when needed.
***
## Running styles
Running styles are returned in `biomechanical_analysis.running_styles` (side view only). They do **not** use `search_key`.
Each object includes `title`, `animal`, `animal_explanation`, `summary`, `explanation`, and `percentage`.
**`title` is the unique identifier** for a running style. It is always one of the English values below (even when `animal`, `summary`, and `explanation` are localized). Use `title` for logic, routing, or mapping to your own labels — the same role `search_key` plays for other biomechanical segments.
An analysis returns **1–2** of these styles; when two are returned, their `percentage` values sum to approximately 100.
All possible states:
| title (UID) | animal | meaning |
| - | - | - |
| `Hop` | Kangaroo | High step frequency, relatively low stance phase |
| `Bounce` | Gazelle | Intermediate cadence, short stance, long flight |
| `Push` | Cheetah | Relatively low cadence, medium stance, more horizontal propulsion |
| `Stick` | Bear | Intermediate cadence, long stance, short flight |
| `Sit` | Ostrich | Intermediate cadence and stance/flight ratio; larger knee flexion at contact |
# Shoe recommendation
Source: https://docs.ochy.io/guides/shoe-recommendation
Three-step flow: submit a questionnaire with optional subject link and analyses, start processing, then poll for ranked shoe matches and parameter recommendations.
This guide describes the **Partner API** shoe recommendation flow. It combines a short runner
questionnaire with one or two **completed** biomechanical analyses (side view and/or back view),
then returns ideal shoe characteristics and a ranked list of catalog matches.
## Prerequisites
* Optional **`subject_id`** in the questionnaire from [Subject management](/guides/subject-management) when you want the recommendation tied to a known subject. If you send it, it must exist for your account.
* At least one **successful** analysis whose `video_id` you will reference:
* `id_side_view_analysis` must point to a completed **side** analysis (`analysis_type` `side_view` or legacy `running` in stored metadata).
* `id_back_view_analysis` must point to a completed **back** analysis (`analysis_type` `back_view` or legacy `pronation`).
* You may pass **one or both** analysis ids. Passing both yields the richest context. If you send
`form.subject_id` and an analysis was started with a `subject_id`, those values must match.
## Shoe catalog
Recommendations only include shoes that are **active** in the catalog your API key uses.
Inactive or missing models are excluded from `best_matching_shoe` and `all_matches`, even when
they would otherwise match the runner profile.
| API key | Catalog used |
| - | - |
| **Organization** key | The active shoes of your organization's catalog. |
| **Self-serve** key (a developer account without an organization) | The shoes you activate in your own catalog in the developer portal. **No shoe is active by default.** |
Things to know about the catalog:
* **An empty catalog is not an error.** With no active shoe, the recommendation still completes
successfully (`status: success`), but no shoe is returned in `best_matching_shoe` or
`all_matches`. Activate shoes to get matches.
* **Self-serve keys have no stock filtering.** Only the active flag decides which shoes are
offered; `use_stock`, if you send it, has no effect.
* **Product links come from your own catalog only.** `product_link` and `shoe_asset.product_page`
in results are the links you set for your own shoes. They are empty or absent until you set
them: Ochy never fills them in from its own catalog.
### Activate shoes in the developer portal
Before you call the API, activate the models you want offered in results in the **Shoes** tab of
the [developer portal](https://developers.ochy.io). Organization admins manage the organization's
catalog. With a self-serve account, you manage your own catalog.
### External product catalogue
To connect an **external product catalogue** instead of (or in addition to) the catalog managed
in Ochy, contact the Ochy team. This is available to **organization** accounts only. That
integration is set up on your account by Ochy — it is not configured through the Partner API
alone.
## Three-step flow
`POST /shoeRecommendation/form` with JSON body `input.form` (runner profile),
optional `id_side_view_analysis`, `id_back_view_analysis`, and optional `favorite_brands`.
The response is **201** with `{ "shoe_recommendation_id": "..." }`.
`POST /shoeRecommendation/start` with `{ "shoe_recommendation_id": "" }`.
The response is **202** with a short confirmation message. Processing runs asynchronously.
`GET /shoeRecommendation/{shoe_recommendation_id}/result`.
While the job is queued or running, the API returns **404** with a JSON body that includes
`status` (`pending` or `processing`). If processing fails, you get **404** with `status: failed`.
On success, **200** returns only the business payload: `recommendations`, `best_matching_shoe`,
`all_matches`, and `user_id`. Each catalog shoe in `best_matching_shoe` and in `all_matches`
includes top-level **`gtins`** and **`mpns`** (arrays of strings, possibly empty), copied from
the catalog model for GTIN/EAN and manufacturer part numbers.
You can also review the recommendations created with your API key, with their analyses and ranked
matches, in the **Shoe recommendations** tab of the [developer portal](https://developers.ochy.io).
## Request body: `input` (with `form`)
The JSON body is a single object `{ "input": { ... } }`. Fields **next to** `form` apply to the
whole recommendation request:
| Field | Type | Required | Default | Allowed values | Description |
| - | - | - | - | - | - |
| `form` | object | **Yes** | — | — | Runner questionnaire (see table below). |
| `id_side_view_analysis` | string | No | — | Any `video_id` of a **successful** `side_view` analysis for your account | Biomechanical input from the side camera. |
| `id_back_view_analysis` | string | No | — | Any `video_id` of a **successful** `back_view` analysis for your account | Biomechanical input from the back camera. |
| `favorite_brands` | string\[] | No | — | Array of non-empty strings (your brand keys) | Optional filter on the shoe catalog. |
At least **one** of `id_side_view_analysis` or `id_back_view_analysis` must be non-empty. Each id
must match an analysis you own, with the correct `analysis_type`, and `source` from the Partner API.
If you send `form.subject_id` and an analysis was created with a `subject_id`, that id must equal `form.subject_id`.
Analyses started before October 2, 2026 are stored in a different workspace from analyses
started since. If `id_side_view_analysis` and `id_back_view_analysis` come from different
workspaces, the request is rejected with **400** and code `analyses_mixed_owner`. Reference only
one of them, or use two analyses started on or after October 2, 2026.
## Questionnaire: `input.form`
All fields below live under `input.form` in the JSON body.
| Field | Type | Required | Default | Allowed values | Description |
| - | - | - | - | - | - |
| `subject_id` | string | No | — | — | Subject identifier from [Subject management](/guides/subject-management) when linking this recommendation to a subject (same value as `subject_id` on analysis start when you link an analysis). |
| `running_experience` | string | **Yes** | — | `beginner`, `novice`, `intermediate`, `advanced` | How long the runner has been running: `beginner` (under 6 months), `novice` (6 months to 1 year), `intermediate` (1 to 3 years), `advanced` (more than 3 years). |
| `running_session` | string | **Yes** | — | `low_frequency`, `moderate_frequency`, `high_frequency`, `very_high_frequency` | Typical training frequency per week (`low_frequency` once or less; `moderate_frequency` 2--3; `high_frequency` 3--5; `very_high_frequency` more than 5). |
| `weekly_km` | string | **Yes** | — | `low_volume`, `moderate_volume`, `high_volume`, `very_high_volume` | Weekly running distance band (`low_volume` up to \~10 km; `moderate_volume` \~10--20 km; `high_volume` \~20--50 km; `very_high_volume` above \~50 km). |
| `shoe_purpose` | string\[] | **Yes** | — | Each element **must** be one of: `jogging`, `racing`, `tempo_training`, `interval_training`. | Intended uses for the new shoes (multi-select). |
| `pain` | string\[] | No | `[]` (empty) | Each element **must** be one of: `back`, `hip`, `knee`, `shin_splints`, `calf`, `ankle`, `achilles_tendon`, `foot` | Current pain or discomfort areas. Omit the field or send `[]` if none. |
| `treadmill` | boolean | **Yes** | — | Only `true` or `false` | Whether the runner trains mostly on a treadmill. |
| `weight` | number | No | `70` | Any number greater than 0 (kilograms) | Body weight. |
| `height` | number | No | `1.70` | Any number greater than 0 (meters) | Standing height. |
| `carbon_plate` | string | No | *(omitted → engine default `unknown`)* | `with_plate`, `without_plate`, `unknown` | Preference regarding a carbon plate in the **new** shoe. |
| `comfort_heel_cushioning` | integer | No | null / omitted | Integer **0**--**100**, or omit / `null` for unknown | Perceived heel cushioning of current shoes. |
| `comfort_forefoot_cushioning` | integer | No | null / omitted | Integer **0**--**100**, or omit / `null` | Perceived forefoot cushioning of current shoes. |
| `comfort_shoe_stability` | integer | No | null / omitted | Integer **0**--**100**, or omit / `null` | Perceived stability of current shoes. |
| `comfort_forefoot_flexibility` | integer | No | null / omitted | Integer **0**--**100**, or omit / `null` | Perceived forefoot flexibility of current shoes. |
| `previous_shoes` | object | No | *(omitted)* | See [Previous shoes object](#previous-shoes) | Optional structured description of the runner's current or last shoes. |
Previous shoes object (previous\_shoes)
When you send `previous_shoes`, each property is optional; only include keys you know. If you send
a key, its value **must** be one of the tokens listed for that key.
| Field | Type | Allowed values | Description |
| - | - | - | - |
| `stability` | string | `motion_control`, `neutral`, `unknown` | Overall stability category of the previous shoe. |
| `drop` | string | `zero_drop`, `low_drop`, `medium_drop`, `high_drop`, `unknown` | Heel-to-toe drop of the previous shoe. |
| `carbon_plate` | string | `with_plate`, `without_plate`, `unknown` | Whether the previous shoe had a rigid / carbon plate. |
| `shoe_weight` | string | `medium`, `light`, `heavy`, `barefoot`, `unknown` | Perceived weight of the previous shoe. |
For request/response examples and full OpenAPI schemas, see the **API reference** tab under **Shoe recommendation**. The **GET** `/shoeRecommendation/{shoe_recommendation_id}/result` page includes a full **200** response example (`partner_success`).
## Result payload: catalog identifiers
On **200** from `GET /shoeRecommendation/{shoe_recommendation_id}/result`, every shoe object returned
as `best_matching_shoe` or as an element of `all_matches` includes:
| Field | Type | Description |
| - | - | - |
| `gtins` | string\[] | GTIN/EAN identifiers linked to the shoe model in the catalog (empty array if none). |
| `mpns` | string\[] | Manufacturer part numbers for model variants (empty array if none). |
## Related guides
* [Analysis workflow](/guides/analysis-workflow) -- how to obtain `video_id` values and poll analysis results.
* [Subject management](/guides/subject-management) -- creating subjects and linking analyses.
# Subject management
Source: https://docs.ochy.io/guides/subject-management
Create and manage subjects to organize analyses by individual runner.
Subjects let you group analyses by individual runner. Create a subject record,
then pass the `subject_id` when starting an analysis to link them together.
Later, retrieve all analyses for a subject in a single call.
## Typical workflow
```mermaid theme={null}
sequenceDiagram
participant You
participant API as Ochy API
You->>API: POST /subjects (runner info)
API-->>You: 201 { subject_id }
You->>API: POST /analysis/start (video + subject_id)
API-->>You: 202 { video_id }
Note over You,API: Repeat for every session
You->>API: GET /analysis/subjects/{subject_id}
API-->>You: 200 { analysis_ids, count }
```
## When to use subjects
Subjects are optional. They are useful when you need to:
* Track analyses per runner across multiple sessions
* Store basic runner metadata (name, email, height, weight) once and reuse it
* List all analyses for a specific runner
If you only run one-off analyses without tracking individual runners, you can skip
subject management entirely.
## Create a subject
All body fields are optional. The server generates a unique `subject_id`.
Height must be in **meters** and weight in **kilograms** (metric system).
```bash cURL theme={null}
curl -X POST "https://partner-api.ochy-prod.com/subjects" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "runner@example.com",
"name": "Jane Smith",
"height": 1.72,
"weight": 62
}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://partner-api.ochy-prod.com/subjects",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"email": "runner@example.com",
"name": "Jane Smith",
"height": 1.72,
"weight": 62,
},
)
subject = response.json()
print(f"Subject created: {subject['subject_id']}")
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://partner-api.ochy-prod.com/subjects",
{
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "runner@example.com",
name: "Jane Smith",
height: 1.72,
weight: 62,
}),
}
);
const { subject_id } = await response.json();
console.log(`Subject created: ${subject_id}`);
```
Response (`201 Created`):
```json theme={null}
{
"subject_id": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-03-20T15:30:00Z"
}
```
## Link an analysis to a subject
Pass `subject_id` as a form field when starting an analysis:
```bash theme={null}
curl -X POST "https://partner-api.ochy-prod.com/analysis/start" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "video=@runner_side.mp4" \
-F "analysis_type=side_view" \
-F "height=1.72" \
-F "weight=62" \
-F "subject_id=550e8400-e29b-41d4-a716-446655440000"
```
The analysis is stored with a reference to the subject. You can then retrieve all
analyses for that subject using the [subject analyses endpoint](#list-analyses-for-a-subject).
When you [list all analyses](/api-reference/analysis/list-analyses), each item includes
the `subject_id` field if a subject was linked.
## List all subjects
```bash cURL theme={null}
curl "https://partner-api.ochy-prod.com/subjects" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```python Python theme={null}
response = requests.get(
"https://partner-api.ochy-prod.com/subjects",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
data = response.json()
for subject in data["subjects"]:
print(f"{subject['id']} - {subject.get('name', 'No name')}")
```
Response:
```json theme={null}
{
"subjects": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "runner@example.com",
"name": "Jane Smith",
"height": 1.72,
"weight": 62,
"created_at": "2025-03-20T15:30:00Z"
}
],
"count": 1
}
```
## Get a single subject
```bash theme={null}
curl "https://partner-api.ochy-prod.com/subjects/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Returns the subject object, or `404` if the subject does not exist or has been deleted.
## Update a subject
Only the provided fields are updated. Omitted fields remain unchanged.
Same units as creation: `height` in **meters**, `weight` in **kilograms**.
```bash theme={null}
curl -X PUT "https://partner-api.ochy-prod.com/subjects/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"weight": 63}'
```
Response:
```json theme={null}
{
"message": "Subject updated"
}
```
## Delete a subject
Subjects are soft-deleted -- the record is kept but excluded from list results.
The endpoint uses `POST` instead of `DELETE` to prevent accidental deletion.
```bash theme={null}
curl -X POST "https://partner-api.ochy-prod.com/subjects/550e8400-e29b-41d4-a716-446655440000/delete" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Response:
```json theme={null}
{
"message": "Subject deleted"
}
```
## List analyses for a subject
Retrieve all analysis IDs linked to a specific subject:
```bash cURL theme={null}
curl "https://partner-api.ochy-prod.com/analysis/subjects/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```python Python theme={null}
response = requests.get(
f"https://partner-api.ochy-prod.com/analysis/subjects/{subject_id}",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
data = response.json()
for analysis in data["analysis_ids"]:
print(f"{analysis['id']} - {analysis['status']}")
```
```javascript JavaScript theme={null}
const res = await fetch(
`https://partner-api.ochy-prod.com/analysis/subjects/${subjectId}`,
{ headers: { Authorization: "Bearer YOUR_API_KEY" } }
);
const { analysis_ids, count } = await res.json();
console.log(`Found ${count} analyses for this subject`);
```
Response:
```json theme={null}
{
"analysis_ids": [
{ "id": "a1b2c3d4", "status": "success" },
{ "id": "e5f6g7h8", "status": "analyzing" }
],
"count": 2
}
```
## Subject fields reference
| Field | Type | Description |
| - | - | - |
| `id` | string | Server-generated UUID. Read-only. |
| `email` | string | Subject email address. Optional. |
| `name` | string | Subject display name. Optional. |
| `height` | number | Subject height in meters (e.g. `1.72`). Optional. |
| `weight` | number | Subject weight in kilograms (e.g. `62`). Optional. |
| `created_at` | string | ISO 8601 timestamp. Read-only. |
# API keys and webhooks
Source: https://docs.ochy.io/guides/webhooks-and-api-keys
Manage API keys in the developer portal, configure webhooks, and receive analysis and low-credit events on your server.
The [Ochy developer portal](https://developers.ochy.io) is the self-service interface for your API
account. Sign in with your self-serve developer account or, for organizations, with an
organization admin account (see [get API access](/get-api-access)).
The portal has the following tabs:
| Tab | What it contains |
| - | - |
| **Integration** | Your API key and webhook configuration. |
| **Usage & Credits** | Credit balance, credit purchase (self-serve accounts), and usage over time. |
| **Latest analyses** | Analyses started with your API key, with an interactive results viewer. |
| **Shoe recommendations** | [Shoe recommendations](/guides/shoe-recommendation) created with your API key, with their analyses and ranked matches. |
| **Shoes** | Shoe catalog for [shoe recommendation](/guides/shoe-recommendation#shoe-catalog). Organization admins manage the organization's catalog; self-serve accounts manage their own. |
| **Settings** | [Data retention](#data-retention) and [low-credit alerts](#low-credit-alerts). |
The API section of the Ochy web app (**Channels > API**) has moved to the developer portal. For
organization scopes, only organization admins can generate, rotate, or revoke API keys and
change webhooks and settings.
## API key management
Your API key authenticates every request to the Partner API. See [authentication](/authentication)
for how to pass it in your requests.
### Generate a key
If you do not have an active key yet:
Sign in to [developers.ochy.io](https://developers.ochy.io) and select the **Integration** tab
(or use the **Get started** checklist).
The portal creates a new key and displays it once. Self-serve developer accounts must verify
their email address before they can generate a key.
Click the copy icon to save the key to your clipboard. Store it in a secure location such as an
environment variable or a secrets manager.
The full key is shown only once, immediately after generation. If you navigate away without
copying it, you will need to rotate the key to get a new one.
### Rotate a key
Rotation revokes the current key and generates a replacement in a single step. Use this when you
need a fresh key without downtime between revoke and generate.
A confirmation dialog warns that existing integrations will stop working immediately.
The old key is revoked and a new key appears in the banner. Copy it before leaving the page.
Replace the old key in every service that calls the Ochy API.
### Revoke a key
Revoking permanently deactivates the key. Any request using the revoked key will receive a
`403 Invalid API Key` error.
A confirmation dialog explains that integrations using this key will stop working.
The key is deactivated. You can generate a new key at any time from the same tab.
## Webhooks overview
Instead of calling `GET /analysis/{analysis_id}/results` repeatedly, you can ask Ochy to **push**
status updates to an HTTPS endpoint you control. Your server receives JSON payloads as the analysis
moves from queued to running, through progress milestones, to success or failure.
Webhooks are optional. [Polling](/guides/analysis-workflow#polling-strategy) remains fully
supported for debugging, gradual rollouts, or when delivery issues occur.
The integration flow:
1. **Register a webhook URL** once for your partner account — from the developer portal or with
[`PUT /webhook`](/api-reference/webhooks/configure-webhook). Use
[`GET /webhook`](/api-reference/webhooks/get-webhook) to read the current URL, `enabled` flag,
signature status, and `updated_at`.
2. **Opt in per analysis** when you call [`POST /analysis/start`](/api-reference/analysis/start-analysis)
with `notify_via_webhook: true`. If you omit it or set it to `false`, no webhook events are sent
for that analysis.
3. **Optionally enable HMAC signatures** so your server can verify that events were sent by Ochy
and that the request body was not modified.
4. **Handle incoming POST requests** on your URL. Each payload includes an event type, status,
percentage, a monotonic `sequence` per analysis, and a timestamp.
Delivery is asynchronous: your endpoint should respond quickly (for example `2xx`) and process the
payload in the background if needed.
Each event is delivered **at most once**: Ochy makes a single attempt with a 10-second timeout
and does **not retry** after a failure, timeout, or non-`2xx` response. Keep a slow fallback
poll for analyses that have not reached a terminal event. See
[delivery guarantees](#delivery-guarantees).
## Configure webhooks
You configure the webhook **once per partner**; you do not pass the URL on every
`POST /analysis/start`.
### From the developer portal
You need an active API key before you can configure a webhook.
Sign in to [developers.ochy.io](https://developers.ochy.io) and scroll to the webhook section
of the **Integration** tab.
Provide an HTTPS URL where Ochy will send POST requests. For example:
`https://your-app.com/webhooks/ochy`.
Click **Save Webhook**. The webhook is enabled automatically.
The URL must use HTTPS and its host must resolve to a public address. URLs that point to a
private, loopback, or link-local address (for example `10.0.0.5`, `127.0.0.1`, or
`169.254.169.254`) are rejected with **400** and code `invalid_webhook_url`.
Once configured, use the toggle switch to enable or disable delivery without removing the URL.
Click **Edit URL** to change the endpoint, then **Update Webhook** to save.
Click **Send Test Event** to send a sample payload to your endpoint. The portal displays the
HTTP status code and response latency so you can verify connectivity.
The test button is only available when the webhook is enabled. Toggle it on first if you need to
test.
### From the API
**`GET /webhook`** returns the stored `url`, `enabled`, and `updated_at` for your partner. If
nothing has been saved yet, the response body is an empty JSON object (`{}`).
```bash theme={null}
curl -s "https://partner-api.ochy-prod.com/webhook" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Use **`PUT /webhook`** with a JSON body:
```json theme={null}
{
"url": "https://client.example.com/webhooks/ochy",
"enabled": true,
"signature": {
"enabled": true
}
}
```
| Field | Type | Description |
| - | - | - |
| `url` | string | HTTPS URL that will receive `POST` requests from Ochy. |
| `enabled` | boolean | When `false`, no webhook traffic is sent until you enable it again. |
| `signature.enabled` | boolean | Optional. When `true`, Ochy signs webhook requests with HMAC-SHA256. |
A successful **`PUT`** returns the same shape as **`GET`**: `url`, `enabled`, and `updated_at`.
If you enable signatures for the first time, the response also includes a `signature.secret`.
Copy and store it immediately; it is not returned by `GET /webhook`.
Example response when a signing secret is generated:
```json theme={null}
{
"url": "https://client.example.com/webhooks/ochy",
"enabled": true,
"updated_at": "2026-06-29T13:25:17Z",
"signature": {
"enabled": true,
"algorithm": "HMAC-SHA256",
"last4": "n6p8",
"created_at": "2026-06-29T13:25:17Z",
"updated_at": "2026-06-29T13:25:17Z",
"secret": "YOUR_GENERATED_SIGNING_SECRET"
}
}
```
Store `signature.secret` securely. Ochy only returns the full signing secret when it is generated
or rotated.
When `notify_via_webhook` is `true` on start, the API checks that your partner webhook is valid
and enabled. If it is not, the request is **not** rejected: the analysis starts with notifications
disabled, and no events are sent for it. The decision to send webhooks for that analysis is fixed
at start time and stored with the analysis.
## Start an analysis with webhooks
Add an optional field to the same multipart request you already use for `POST /analysis/start`:
| Field | Type | Default | Description |
| - | - | - | - |
| `notify_via_webhook` | boolean | `false` | If `true`, progress and completion events are delivered to your configured webhook URL for this analysis only. |
Example (cURL):
```bash theme={null}
curl -X POST "https://partner-api.ochy-prod.com/analysis/start" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "video=@runner_side.mp4" \
-F "analysis_type=side_view" \
-F "notify_via_webhook=true"
```
## Events and payload format
For each analysis, events are emitted when:
* processing **starts** (status moves to `analyzing`);
* **progress** crosses each **10%** boundary from 10% through 90% (if progress jumps, all crossed
milestones are sent in order);
* the analysis **completes** successfully (`success`, 100%);
* the analysis **fails** (`failed`), at any progress value.
| `event_type` | Typical `status` | Notes |
| - | - | - |
| `analysis.started` | `analyzing` | First processing signal for the run. |
| `analysis.progress` | `analyzing` | Progress milestones at 10%, 20%, … 90%. |
| `analysis.completed` | `success` | Final success; `percentage` is 100. |
| `analysis.failed` | `failed` | Terminal failure; `percentage` reflects last known progress. |
Ochy sends **`POST`** requests to your webhook URL with a JSON body like:
```json theme={null}
{
"analysis_id": "abc123",
"event_type": "analysis.progress",
"status": "analyzing",
"percentage": 40,
"sequence": 5,
"timestamp": "2026-03-23T10:45:00Z"
}
```
| Field | Description |
| - | - |
| `analysis_id` | Same identifier as `video_id` from the start endpoint. |
| `event_type` | One of `analysis.started`, `analysis.progress`, `analysis.completed`, `analysis.failed`. |
| `status` | Current analysis status (`analyzing`, `success`, or `failed`). |
| `percentage` | Progress from 0 to 100. |
| `sequence` | **Per-analysis** monotonic counter. Use it to order and deduplicate events. |
| `timestamp` | Time of the event in UTC (ISO 8601). |
**Completed:**
```json theme={null}
{
"analysis_id": "abc123",
"event_type": "analysis.completed",
"status": "success",
"percentage": 100,
"sequence": 11,
"timestamp": "2026-03-23T10:46:30Z"
}
```
**Failed:**
```json theme={null}
{
"analysis_id": "abc123",
"event_type": "analysis.failed",
"status": "failed",
"percentage": 60,
"sequence": 7,
"timestamp": "2026-03-23T10:46:30Z"
}
```
## Verify webhook signatures
Webhook signatures are optional but recommended for production integrations. Enable them from the
developer portal or with `signature.enabled: true` in `PUT /webhook`. When signatures are enabled,
Ochy adds two HTTP headers to every webhook request, including
[`credits.low`](#low-credit-alerts) events:
| Header | Description |
| - | - |
| `X-Ochy-Timestamp` | Unix timestamp in seconds, generated when the request is sent. |
| `X-Ochy-Signature` | Hex-encoded HMAC-SHA256 signature. |
The signed value is:
```text theme={null}
.
```
To verify a webhook:
1. Read the raw request body bytes exactly as received.
2. Read `X-Ochy-Timestamp` and `X-Ochy-Signature`.
3. Reject old timestamps (for example, more than 5 minutes away from your server time).
4. Compute `HMAC-SHA256` with your webhook signing secret over `.`.
5. Compare the expected signature with `X-Ochy-Signature` using a constant-time comparison.
Do not parse and re-serialize the JSON before verifying the signature. JSON whitespace or key
ordering changes will produce a different signature. Verify against the raw request body.
### Python example
```python theme={null}
import hashlib
import hmac
import time
def verify_ochy_webhook(secret: str, timestamp: str, signature: str, raw_body: bytes) -> bool:
# Reject replayed requests. Adjust the tolerance to your infrastructure.
if abs(time.time() - int(timestamp)) > 300:
return False
signed_payload = timestamp.encode("utf-8") + b"." + raw_body
expected = hmac.new(
secret.encode("utf-8"),
signed_payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
```
### Node.js example
```js theme={null}
import crypto from "crypto";
function verifyOchyWebhook(secret, timestamp, signature, rawBodyBuffer) {
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (ageSeconds > 300) {
return false;
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBodyBuffer)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
}
```
## Rotate the signing secret
Use **`POST /webhook/signature/rotate`** to generate a new signing secret:
```bash theme={null}
curl -X POST "https://partner-api.ochy-prod.com/webhook/signature/rotate" \
-H "Authorization: Bearer YOUR_API_KEY"
```
The response includes the new `secret` once:
```json theme={null}
{
"enabled": true,
"algorithm": "HMAC-SHA256",
"last4": "7Tq2",
"created_at": "2026-06-29T13:25:17Z",
"updated_at": "2026-06-29T14:10:00Z",
"secret": "YOUR_NEW_SIGNING_SECRET"
}
```
Update your webhook receiver to use the new secret.
## Ordering and out-of-order delivery
Webhook delivery is asynchronous. In rare cases an older event might arrive after a newer one.
* Each payload includes a **`sequence`** that increases for that `analysis_id`.
* **Process events in `sequence` order**; if you receive a payload whose `sequence` is less than or
equal to the last one you applied for that analysis, you can **ignore** it.
Ochy also uses internal ordering so that events for a given analysis are published in sequence;
this reduces but does not eliminate the need for client-side ordering using `sequence`.
## Delivery guarantees
Ochy sends each event **once**, with a 10-second timeout. If your endpoint is unreachable, slow,
or answers with a non-`2xx` status, the event is dropped: there is **no retry policy**.
This is intentional. Progress events are only useful when they arrive on time; an update delivered
a few seconds late is already outdated. A retry policy would also mean repeatedly calling an
endpoint that may be misconfigured (for example a wrong URL).
To build a reliable integration:
* Treat webhooks as a fast notification, not as a guaranteed log. Gaps in `sequence` are normal.
* For every analysis without a terminal event (`analysis.completed` or `analysis.failed`), poll
`GET /analysis/{analysis_id}/results` at a slow interval such as every 30 seconds, within your
[rate limit budget](/guides/limits-and-reliability#budget-your-polling).
* Events only carry `analysis_id`, which is the `video_id` returned by `POST /analysis/start`. They
contain no client-side reference. If you never received the `video_id`, retry the start request
with the same `Idempotency-Key` to get it back (see
[recovering a lost `video_id`](/guides/limits-and-reliability#recovering-a-lost-video_id)).
## Fallback: polling
You can always call:
```
GET /analysis/{analysis_id}/results
```
This endpoint continues to return the current status and percentage (and full results when
`status` is `success`), whether or not webhooks are enabled. Use it for integration testing,
manual checks, or when you need a single source of truth after delivery issues.
## Usage and credits
The **Usage & Credits** tab of the [developer portal](https://developers.ochy.io) shows:
* **Credits** — your remaining analysis credits for the scope of your API key (personal or
organization). Each analysis consumes one credit when started (see
[authentication](/authentication#credits)).
* **Usage overview** — for the selected period: accepted, successful, and failed analyses,
analyses still processing, the success rate, and a chart of side view and back view analyses
over time. Only analyses started through the Partner API are counted.
To add credits: self-serve developers buy them in the same tab (see
[get API access](/get-api-access#pricing)); organizations go through their Ochy contact.
Analyses started with your API key are listed in the **Latest analyses** tab, where you can open
each result in an interactive viewer. They do not appear in the Ochy mobile app or web app.
## Low-credit alerts
Get warned before your balance runs out. Configure the alert in the portal **Settings** tab or
with [`PUT /settings/low-credit-alerts`](/api-reference/settings/update-low-credit-alerts):
| Field | Description |
| - | - |
| `enabled` | Turns the alert on or off. |
| `threshold` | Number of credits, from 0 to 10000. The alert fires when an analysis brings the balance **to or below** the threshold. `0` never fires. |
| `webhookUrl` | Optional HTTP(S) URL that receives a `credits.low` event. It is separate from the analysis webhook URL and follows the same rules (public host only). |
When the alert fires:
* An **email** is sent to the person who last saved the alert settings.
* If `webhookUrl` is set, Ochy sends a `POST` request with a `credits.low` event to it.
The alert fires **once per crossing**: it rearms when the balance rises above the threshold again
(for example after buying credits), and it fires at most once per hour. Only credits consumed by
analyses trigger it. Scopes with unlimited credits cannot configure alerts.
```json theme={null}
{
"event_id": "8f14e45f-ceea-467a-9575-36a1a4d3b8c2",
"event_type": "credits.low",
"scope_type": "organization",
"scope_id": "org_abc123",
"threshold": 100,
"balance_before": 101,
"balance_after": 100,
"trigger": "analysis_debit",
"timestamp": "2026-10-05T10:45:00.000Z"
}
```
| Field | Description |
| - | - |
| `event_id` | Unique identifier of the alert (UUID). Use it to deduplicate. |
| `event_type` | Always `credits.low`. |
| `scope_type` | `personal` or `organization`. |
| `scope_id` | Credit scope the alert belongs to. |
| `threshold` | Threshold configured when the alert fired. |
| `balance_before` / `balance_after` | Balance before and after the analysis that crossed the threshold. |
| `trigger` | What consumed the credit. Currently always `analysis_debit`. |
| `timestamp` | Time of delivery in UTC (ISO 8601). |
When [webhook signatures](#verify-webhook-signatures) are enabled, `credits.low` requests carry the
same `X-Ochy-Timestamp` and `X-Ochy-Signature` headers, signed with the same secret. Delivery is
attempted **once** with a 10-second timeout and redirects are not followed, so respond with a
`2xx` quickly and keep an eye on your balance in the portal as a fallback.
## Data retention
By default, Ochy keeps analysis videos and data indefinitely. In the portal **Settings** tab, you
can set two independent retention periods:
| Setting | What is deleted | Options |
| - | - | - |
| **Video retention** | Input video, analyzed video, and thumbnail. | Off (keep forever), 7, 30, 90, 180, or 365 days |
| **Analysis data retention** | The analysis itself and its result files. | Off (keep forever), 7, 30, 90, 180, or 365 days |
Retention applies only to analyses **started after you save** the settings; the expiry date is
fixed when the analysis starts. Expired content is deleted by a daily job.
What changes in the API responses:
* After the video retention period, [`GET /analysis/{analysis_id}/results`](/api-reference/analysis/get-analysis-results)
still returns `biomechanical_analysis`, but `thumbnail` and `output_video` are omitted and
`video_expired` is `true`.
* After the analysis data retention period, the analysis is deleted and requests for it return
**404**.
Deletion is permanent. Copy any results you need to keep to your own storage before they expire.
## See also
* [Analysis workflow](/guides/analysis-workflow) — video requirements, status codes, and polling
* [`GET /webhook`](/api-reference/webhooks/get-webhook) — read current webhook settings
* [`PUT /webhook`](/api-reference/webhooks/configure-webhook) — register or update your webhook URL
* [`POST /webhook/signature/rotate`](/api-reference/webhooks/rotate-webhook-signature) — rotate your webhook signing secret
* [`GET /settings/low-credit-alerts`](/api-reference/settings/get-low-credit-alerts) — read low-credit alert settings
* [`PUT /settings/low-credit-alerts`](/api-reference/settings/update-low-credit-alerts) — configure low-credit alerts
* [`POST /analysis/start`](/api-reference/analysis/start-analysis) — `notify_via_webhook` field
* [Limits and reliability](/guides/limits-and-reliability) — rate limits, idempotent retries, recovery
# Introduction
Source: https://docs.ochy.io/introduction
Integrate running biomechanical analysis into your application with the Ochy Partner API.
Ochy is a biomechanical analysis platform that turns a short video of a runner into
detailed performance data -- running metrics, joint angles, foot landing patterns,
pronation analysis, and running style classification.
The Partner API gives you programmatic access to this analysis engine. Upload a video,
wait for processing, and retrieve structured results that you can display in your own
product.
## What you can do
Capture a runner from the side to get biomechanical scores, running metrics (speed,
step length, cadence, ground contact time), joint angles, overstride analysis, foot
landing patterns, and running style classification.
Capture a runner from behind to get pronation/supination data, pelvic drop, knee
adduction, hip adduction, and gait cycle metrics for each leg.
Create subjects to group analyses by individual runner. Track metadata,
link analyses, and retrieve per-subject history.
Combine a runner questionnaire with completed side and/or back analyses to obtain
ideal shoe characteristics and ranked catalog matches (three-step flow).
## Video requirements
Upload an MP4, MOV, AVI, or WEBM file -- max 10 seconds, max 30 MB. Required orientation
(landscape or portrait) depends on the analysis type and whether the runner is on a treadmill.
See [example videos](/guides/example-videos) for the full orientation matrix, resolution
guidance, and sample input/output clips.
## How it works
Send a short video of a runner to the
[start analysis](/api-reference/analysis/start-analysis) endpoint.
Make sure the video meets the [orientation and format requirements](#video-requirements)
for the chosen analysis type. You receive a `video_id` immediately.
Use the [get results](/api-reference/analysis/get-analysis-results) endpoint
with your `video_id`. The status progresses from `pending` to `analyzing` to `success`.
You can also use [webhooks](/guides/webhooks-and-api-keys#webhooks-overview) to receive pushed updates instead of
polling only.
Once the analysis is complete, the results endpoint returns biomechanical data,
signed URLs for the annotated video and thumbnail. You can also fetch
[calculated metrics](/api-reference/analysis/get-calculated-data)
and [raw frame data](/api-reference/analysis/get-raw-data) separately.
## Use with AI coding agents
This documentation is designed to be **agent-friendly**. The entire API is described
by an OpenAPI 3.0 specification with detailed descriptions, typed schemas, and
realistic examples for every endpoint and response -- making it easy for AI coding
assistants to understand and generate accurate integration code.
Two machine-readable files are available at the root of this documentation site
(same access code as the docs):
| File | URL | Purpose |
| - | - | - |
| **llms.txt** | `https://docs.ochy.io/llms.txt` | Documentation index -- lets an AI agent discover all available pages |
| **openapi.yaml** | `https://docs.ochy.io/openapi.yaml` | Full API specification -- endpoints, parameters, schemas, and examples |
### Setup
Save the OpenAPI spec to your project so AI tools can reference it locally:
Download openapi.yaml and
place it at the root of your project (or any convenient path).
Point your coding assistant at the local file. See the tool-specific
instructions below.
The spec contains every endpoint, parameter, schema, and realistic example
response -- your AI tool will generate accurate integration code from it.
### Cursor
Reference the local spec file in chat with **`@openapi.yaml`**, then ask Cursor
to generate integration code.
For **permanent context**, create a Cursor rule at `.cursor/rules/ochy-api.mdc`:
```markdown theme={null}
# Ochy Partner API
When working with the Ochy API, refer to the OpenAPI spec at the project root
for endpoints, parameters, and response schemas:
- Spec file: @openapi.yaml
- Base URL: https://partner-api.ochy-prod.com
- Auth: API key passed in the `Authorization: Bearer` header (or `X-API-Key`)
- All endpoints use metric units: height in meters, weight in kilograms
```
### Other AI tools
| Tool | How to use |
| - | - |
| **GitHub Copilot** | Open `openapi.yaml` in a tab or reference it in chat with `#file:openapi.yaml` |
| **ChatGPT / Claude** | Upload the spec file, then ask for integration code |
| **Windsurf** | Reference `openapi.yaml` in chat with `@openapi.yaml` |
| **Cline / Aider** | Point at the spec file in your project root |
## Next steps
Register self-serve, verify your email for a free analysis credit, and buy
prepaid credits at 2 € per analysis.
Run your first analysis in under 5 minutes.
Learn how API key authentication works.
Understand the full analysis lifecycle, video constraints, and polling strategy.
Receive progress and completion events on your server without polling.
Organize analyses by individual runner with subjects.
Questionnaire plus analyses to get ranked shoe matches and parameter recommendations.
Browse all endpoints with request and response schemas.
# Quickstart
Source: https://docs.ochy.io/quickstart
Run your first biomechanical analysis in under 5 minutes.
## Prerequisites
* An Ochy account with an API key. Register self-serve at
[developers.ochy.io](https://developers.ochy.io/register): verifying your email grants 1 free
analysis credit, so this quickstart costs nothing. See [get API access](/get-api-access) for the
full walkthrough.
* A short video of a runner (MP4, MOV, AVI, or WEBM -- max 30 MB, 10 seconds).
## 1. Start an analysis
Upload your video and provide basic runner parameters. Replace `YOUR_API_KEY` with your
actual key.
```bash cURL theme={null}
curl -X POST "https://partner-api.ochy-prod.com/analysis/start" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "video=@runner_side.mp4" \
-F "analysis_type=side_view" \
-F "height=1.75" \
-F "weight=68" \
-F "language=en" \
-F "treadmill=false"
```
```python Python theme={null}
import requests
response = requests.post(
"https://partner-api.ochy-prod.com/analysis/start",
headers={"Authorization": "Bearer YOUR_API_KEY"},
files={"video": open("runner_side.mp4", "rb")},
data={
"analysis_type": "side_view",
"height": 1.75,
"weight": 68,
"language": "en",
"treadmill": "false",
},
)
data = response.json()
video_id = data["video_id"]
print(f"Analysis started: {video_id}")
```
```javascript JavaScript theme={null}
const form = new FormData();
form.append("video", fs.createReadStream("runner_side.mp4"));
form.append("analysis_type", "side_view");
form.append("height", "1.75");
form.append("weight", "68");
form.append("language", "en");
form.append("treadmill", "false");
const response = await fetch(
"https://partner-api.ochy-prod.com/analysis/start",
{
method: "POST",
headers: { Authorization: "Bearer YOUR_API_KEY" },
body: form,
}
);
const { video_id } = await response.json();
console.log(`Analysis started: ${video_id}`);
```
You receive a `202 Accepted` response with a `video_id`:
```json theme={null}
{
"message": "Analysis started for ID: a1b2c3d4",
"video_id": "a1b2c3d4"
}
```
## 2. Get results
There are two ways to receive analysis results: **polling** or **webhooks**.
Use the `video_id` to check the analysis status. Poll every few seconds until the
status reaches `success`.
```bash cURL theme={null}
curl "https://partner-api.ochy-prod.com/analysis/a1b2c3d4/results" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```python Python theme={null}
import time
while True:
result = requests.get(
f"https://partner-api.ochy-prod.com/analysis/{video_id}/results",
headers={"Authorization": "Bearer YOUR_API_KEY"},
).json()
print(f"Status: {result['status']} ({result.get('percentage', 0)}%)")
if result["status"] == "success":
break
if result["status"] == "failed":
raise Exception("Analysis failed")
time.sleep(3)
```
```javascript JavaScript theme={null}
let result;
do {
await new Promise((r) => setTimeout(r, 3000));
const res = await fetch(
`https://partner-api.ochy-prod.com/analysis/${video_id}/results`,
{ headers: { Authorization: "Bearer YOUR_API_KEY" } }
);
result = await res.json();
console.log(`Status: ${result.status} (${result.percentage ?? 0}%)`);
} while (result.status !== "success" && result.status !== "failed");
```
While processing, you see `202 Accepted` with status `analyzing`:
```json theme={null}
{
"video_id": "a1b2c3d4",
"status": "analyzing",
"percentage": 45
}
```
When complete, the response includes the full biomechanical analysis, signed URLs
for the annotated video and thumbnail.
Instead of polling, you can have Ochy push status updates to your server. First,
[configure a webhook URL](/guides/webhooks-and-api-keys#from-the-developer-portal) in the
developer portal or via [`PUT /webhook`](/api-reference/webhooks/configure-webhook), then
pass `notify_via_webhook=true` when starting the analysis:
```bash theme={null}
curl -X POST "https://partner-api.ochy-prod.com/analysis/start" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "video=@runner_side.mp4" \
-F "analysis_type=side_view" \
-F "height=1.75" \
-F "weight=68" \
-F "notify_via_webhook=true"
```
Ochy sends `POST` requests to your endpoint as the analysis progresses:
```json theme={null}
{
"analysis_id": "a1b2c3d4",
"event_type": "analysis.completed",
"status": "success",
"percentage": 100,
"sequence": 11,
"timestamp": "2026-03-23T10:46:30Z"
}
```
Your endpoint should respond with a `2xx` status and process the payload in the background.
See the [API keys and webhooks guide](/guides/webhooks-and-api-keys) for the full event list,
payload reference, and ordering details.
## 3. Get calculated metrics
For structured numerical data, call the calculated data endpoint:
```bash cURL theme={null}
curl "https://partner-api.ochy-prod.com/analysis/a1b2c3d4/results/calculated_data" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```python Python theme={null}
metrics = requests.get(
f"https://partner-api.ochy-prod.com/analysis/{video_id}/results/calculated_data",
headers={"Authorization": "Bearer YOUR_API_KEY"},
).json()
print(f"Speed: {metrics['metrics']['speed_meter_per_second']} m/s")
print(f"Cadence: {metrics['metrics']['step_frequency_steps_per_min']} spm")
```
Example response for a side view analysis:
```json theme={null}
{
"metrics": {
"speed_meter_per_second": 2.123,
"step_length_meter": 1.15,
"pace_min_per_km": 7.511,
"step_frequency_steps_per_min": 110,
"ground_contact_time_ms": 280,
"time_of_flight_ms": 120
},
"overstride": {
"mean_angle": 3.47,
"score": "score_good"
},
"total_score": 0.588
}
```
## Next steps
* Read the [analysis workflow guide](/guides/analysis-workflow) to understand video
requirements, analysis types, and the full polling lifecycle.
* Use [subject management](/guides/subject-management) to organize analyses by individual runner.
* Browse the [API reference](/api-reference/analysis/start-analysis) for complete request and response schemas.
* Review [authentication](/authentication) for details on API key management and credits.