Errors

Every failure, on every endpoint, in every case, is one envelope. code is what a program branches on — one of twelve values; message is a complete sentence written for a person and may be shown as-is; detail is always present, {} when there is nothing to add, and its keys are fixed per code; request_id equals the X-Request-Id response header and is the key the log line is under — quote it.

{
  "error": {
    "code": "quota_exceeded",
    "message": "This account has used its monthly transcription allowance.",
    "detail": {
      "limit_seconds": 36000,
      "used_seconds": 36012
    },
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

Retry-After is sent on every 429 and every 503, always as delta seconds. Retry a 429 rate_limited, a 503 and — with backoff — a 500; never any other 4xx.

codeHTTPRetry?
invalid_request400no
unauthenticated401no
forbidden403no
not_found404no
conflict409sometimes
payload_too_large413no
unsupported_media_type415no
unprocessable_source422no
rate_limited429after Retry-After
quota_exceeded429yes, but not soon
internal_error500yes, with backoff
engine_unavailable503after Retry-After

invalid_request400

What happened

A malformed body; an unknown field; a missing required field; a value outside an enumeration; a source object that is not exactly one of the three shapes; a bad part_index; a missing etag in multipart mode; a sha256 that is not 64 lowercase hex; a format outside output_format; a malformed or expired cursor; a limit outside [1, 100].

What to do

Fix the request. Every failing field is named in one response — never one at a time. Retryable: no.

Example

{
  "error": {
    "code": "invalid_request",
    "message": "The field source.url is not a recognised YouTube URL shape.",
    "detail": {
      "field": "source.url",
      "reason": "is not a recognised YouTube URL shape"
    },
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

unauthenticated401

What happened

No Authorization header and no session cookie; a token that does not match a live key; a revoked key; an expired session.

What to do

Send a live key as Authorization: Bearer sk_live_…. detail is deliberately empty: distinguishing "no such key" from "revoked" would be an oracle. Retryable: no.

Example

{
  "error": {
    "code": "unauthenticated",
    "message": "This request carried no valid credential.",
    "detail": {},
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

forbidden403

What happened

The key lacks the route's scope; a cookie-authenticated state-changing request from a foreign Origin; a destination url that is not https:// or resolves into a private network.

What to do

Create a key with the scope detail.required_scope names, or fix the destination URL. Never 404: the resource exists and you are authenticated. Retryable: no.

Example

{
  "error": {
    "code": "forbidden",
    "message": "This key does not have the runs:write scope.",
    "detail": {
      "required_scope": "runs:write",
      "scopes": [
        "runs:read"
      ]
    },
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

not_found404

What happened

An id that does not exist, or exists in another account. The two are indistinguishable by design.

What to do

Check the id. Do not retry. Retryable: no.

Example

{
  "error": {
    "code": "not_found",
    "message": "There is no run with that id in this account.",
    "detail": {
      "resource": "run",
      "id": "0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b"
    },
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

conflict409

What happened

Cancelling a run that is already terminal; a part reported against an upload that is not open; completing an upload whose parts do not reconcile; aborting a complete upload; requesting a transcript for a run that is not done; delivering a run that is not terminal or to a disabled destination.

What to do

Read detail.status: it is the resource's actual state, and that decides whether waiting helps. A transcript on a running run: poll and try again. A cancel on a done run: nothing to do. Retryable: sometimes.

Example

{
  "error": {
    "code": "conflict",
    "message": "The transcript is not ready: the run is still running.",
    "detail": {
      "status": "running"
    },
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

payload_too_large413

What happened

declared_bytes above the account byte ceiling; received_bytes crossing it at a part report; a request body above 1 MiB.

What to do

The ceiling is in detail.limit_bytes. Reduce the media or ask the operator for a higher ceiling. Retryable: no.

Example

{
  "error": {
    "code": "payload_too_large",
    "message": "This upload declares 3.2 GB and the ceiling for this account is 2.1 GB.",
    "detail": {
      "limit_bytes": 2147483648,
      "declared_bytes": 3221225472
    },
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

unsupported_media_type415

What happened

A request body without Content-Type: application/json; an Accept header that admits nothing this endpoint can produce.

What to do

Send Content-Type: application/json. Retryable: no.

Example

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Request bodies must be sent as application/json.",
    "detail": {
      "expected": "application/json"
    },
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

unprocessable_source422

What happened

A syntactically valid request whose source cannot be worked with: an unrecognised YouTube URL; an advisory duration above the ceiling; an upload_id that belongs to another account or is already consumed by a run.

What to do

Read detail.reason. Fix the source; a retry with the same source will fail the same way. Retryable: no.

Example

{
  "error": {
    "code": "unprocessable_source",
    "message": "That is not a YouTube URL this service recognises.",
    "detail": {
      "reason": "unrecognised_youtube_url"
    },
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

rate_limited429

What happened

The request rate ceiling for this key or account was exceeded. Nothing was consumed.

What to do

Wait Retry-After seconds and send the same request. detail.scope says whether the key or the account ceiling answered. Pace on the RateLimit-* headers so you never get here. Retryable: after Retry-After.

Example

{
  "error": {
    "code": "rate_limited",
    "message": "This key has sent more than 60 requests in the last minute.",
    "detail": {
      "scope": "key",
      "limit": 60,
      "window_seconds": 60
    },
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

quota_exceeded429

What happened

The account's monthly seconds allowance is spent; the concurrent-open-upload ceiling is reached; the rolling daily byte ceiling is reached; more than 20 destinations.

What to do

Retry-After is honest and can be weeks. An allowance is spent; time or an operator changes it. Do not loop on it. Retryable: yes, but not soon.

Example

{
  "error": {
    "code": "quota_exceeded",
    "message": "This account has used its monthly transcription allowance.",
    "detail": {
      "limit_seconds": 36000,
      "used_seconds": 36012
    },
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

internal_error500

What happened

An unhandled exception. The message never carries it.

What to do

Retry with exponential backoff. Quote request_id if it persists — it is the key the log line is under. Retryable: yes, with backoff.

Example

{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong on our side. Quote the request id if it happens again.",
    "detail": {},
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

engine_unavailable503

What happened

The Modal app cannot be reached and no live worker can claim; a storage provider will not issue a presigned URL; the operator has paused intake — in which case message is the operator's written reason, verbatim.

What to do

Wait Retry-After (30) and back off exponentially. Read message when detail.reason is intake_paused: it says what to expect. Retryable: after Retry-After.

Example

{
  "error": {
    "code": "engine_unavailable",
    "message": "No transcription capacity is available right now. Try again in a minute.",
    "detail": {
      "reason": "no_capacity"
    },
    "request_id": "req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"
  }
}

A failed run is not an HTTP error

An API error is about your request; error_class on a run is about the work. POST /v1/runs answered 201: the request was right and the work was accepted. If the source turns out to be members-only, or scheduled, or has no audio track, the run reaches status: "failed" with an error_class naming why, and every response about that run is a normal 200. A client that treats a failed run as an HTTP failure will retry forever against a source that will never work; a client that treats a 4xx as a failed run will wait for a webhook that will never come.

API errorFailed run
Wherethe response to this requestthe run resource, later
Shape{ "error": { "code": … } } with a 4xx/5xx{ "status": "failed", "error_class": … } with a 200
Branch onerror.code — twelve valueserror_class — nineteen values, the reference lists them
Retry?only 429, 500, 503, per Retry-Afteronly premiere, as a new run after retry_after