Skip to content

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 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.
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.

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"
}
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.