Errors
The API uses conventional HTTP status codes. Broadly: 2xx succeeded, 4xx
failed on something in your request or your account, 5xx is us.
Status codes
Section titled “Status codes”| Status | Meaning |
|---|---|
200 201 |
Worked. |
400 |
Validation failed, or a typed detail code. Fix the request; retrying unchanged never helps. |
401 |
Missing, invalid, or no-longer-entitled credential. |
402 |
Understood and well-formed, but your plan will not allow it. |
403 |
Authenticated, but the role or scheme gate refused — e.g. a human-only endpoint. |
404 |
Not found — or hidden. Another team’s data returns 404, never 403, so the API never confirms that it exists. |
409 |
State collision — a re-analysis already running, an email already in use. |
410 |
Was valid, now gone. An expired share token. |
429 |
Only POST /api/me/verify-email/ emits this, after five wrong codes. The rest of the API is not throttled. |
503 |
A dependency was unavailable. Safe to retry with backoff. |
The codes to branch on
Section titled “The codes to branch on”| Status | detail |
What happened | What to do |
|---|---|---|---|
401 |
Invalid API key |
Unknown, revoked or mistyped. | Stop. Retrying never helps — the credential is wrong. |
401 |
API keys require the Organization plan or higher |
The key is fine; the plan no longer carries API access. Every call fails from this moment. | Stop and alert the customer. This is billing, not code. Keys are not deleted — they work again when the plan is restored. |
403 |
This endpoint is not available to API-key callers. |
A human-only endpoint. | Do not retry with a key. See what a key can reach. |
402 |
quota_exhausted |
Out of analyses for the period. | Queue and retry after reset_at, or have the customer upgrade. Nothing was created — no session, no charge. |
402 |
subscription_suspended |
Payment failed and no live tier remains. Raised before quota is touched. | Stop uploading. The customer must fix payment. |
402 |
client_limit_reached |
Creating an athlete would exceed the plan’s ceiling. | Reuse an existing athlete, or upgrade. |
400 |
{"exercise": ["'…' is not a supported exercise for activity '…'"]} |
Exact-match miss. | Fix the string against the catalogue. Never retry as-is. |
400 |
code: "unsupported_camera_view" |
That pair has no analyser for that plane. Carries supported_views. |
Send a supported view, or re-film. |
400 |
{"id": ["session_id_taken"]} |
That upload-intent id was already consumed — usually a retry after a dropped 201. |
Check whether the session exists before re-creating it. |
400 |
video_url must be an http(s) URL — … |
A placeholder, a bare path, or another team’s media. | Send the public_url from the upload intent, unmodified. |
503 |
enqueue_failed |
Quota was debited but the queue was unreachable. | Safe to retry. |
The 402s carry context
Section titled “The 402s carry context”Every 402 returns the same shape, so one handler covers all of them:
{ "detail": "quota_exhausted", "reset_at": "2026-10-01T00:00:00Z", "current_plan": "organization", "current_role": "organization"}The distinction that matters most
Section titled “The distinction that matters most”flowchart TB
E["402 Payment Required"] --> D{"read detail"}
D -->|quota_exhausted| Q["account healthy,<br/>allowance used"]
D -->|subscription_suspended| S["account in dunning"]
Q --> QA["queue the work,<br/>retry after reset_at"]
S --> SA["stop and escalate —<br/>a person must fix payment"]
quota_exhausted and subscription_suspended are both 402 and both read as
“payment required” from outside. They are not the same:
quota_exhausted— the account is healthy and has simply used its allowance. Waiting fixes it. Queue the work.subscription_suspended— the account is in dunning. Waiting fixes nothing; a person has to update a payment method. Stop and escalate.
Treating the second like the first means a silently stalled integration and a customer who finds out from you rather than from us.