# Errors

> Every API error code with its HTTP status, what it means, what to do and whether to retry. Plus the job and stage failure codes.

Source: https://www.framequery.com/docs/errors (updated 2026-10-13)

## The error envelope

Every error response has the same shape:

```json
{
  "error": "callbackUrl must be a public HTTPS URL.",
  "code": "INVALID_CALLBACK_URL",
  "docUrl": "https://framequery.com/docs/errors#INVALID_CALLBACK_URL",
  "param": "callbackUrl",
  "requestId": "3c9a7e1f5b2d4068"
}
```

| Field | Notes |
|---|---|
| `code` | Stable identifier. Branch on this |
| `error` | Human-readable message. It can change; do not parse it |
| `docUrl` | This page, at the code's anchor |
| `param` | The request field or query parameter at fault, as a JSON path (`fileName`, `audioTracks[2].offsetMs`, `clips[3].sourceUrl`, `metadata.assetId`, `cursor`). Present only when one field is to blame |
| `requestId` | Also in the `X-Request-Id` response header. Quote it to support |

Some codes add fields, listed below (for example `state` and `reason` on `RESULTS_UNAVAILABLE`, `maxUploadBytes` on `FILE_TOO_LARGE`).

## When to retry

- **Retry** every `429` and `503`, and `409` `IDEMPOTENCY_IN_PROGRESS` and `RESULTS_NOT_READY`. All of them carry `Retry-After` in seconds: wait that long, then retry, reusing the same `Idempotency-Key` on creates.
- **Back off and retry** `500 INTERNAL_ERROR` (exponential backoff, same idempotency key). It has no `Retry-After`.
- **Do not retry** anything else unchanged. Every other `4xx` is final for that request: fix it first.

The SDKs follow these rules automatically, honouring `Retry-After` up to 60 seconds.

## Summary

| Code | HTTP | Retry? |
|---|---|---|
| [`INVALID_REQUEST`](#INVALID_REQUEST) | 400 | No |
| [`INVALID_URL`](#INVALID_URL) | 400 | No |
| [`URL_NOT_ALLOWED`](#URL_NOT_ALLOWED) | 400 | No |
| [`INVALID_CALLBACK_URL`](#INVALID_CALLBACK_URL) | 400 | No |
| [`UNAUTHORIZED`](#UNAUTHORIZED) | 401 | No |
| [`INSUFFICIENT_CREDITS`](#INSUFFICIENT_CREDITS) | 402 | After topping up |
| [`INSUFFICIENT_SCOPE`](#INSUFFICIENT_SCOPE) | 403 | No (reserved) |
| [`ACCOUNT_SUSPENDED`](#ACCOUNT_SUSPENDED) | 403 | No |
| [`NOT_FOUND`](#NOT_FOUND) | 404 | No |
| [`RESULTS_UNAVAILABLE`](#RESULTS_UNAVAILABLE) | 404 | No, never |
| [`JOB_NOT_CANCELLABLE`](#JOB_NOT_CANCELLABLE) | 409 | No |
| [`IDEMPOTENCY_IN_PROGRESS`](#IDEMPOTENCY_IN_PROGRESS) | 409 | Yes, after `Retry-After` (2 s) |
| [`RESULTS_NOT_READY`](#RESULTS_NOT_READY) | 409 | Yes, after `Retry-After` (15 s) |
| [`FILE_TOO_LARGE`](#FILE_TOO_LARGE) | 413 | No |
| [`UNSUPPORTED_FORMAT`](#UNSUPPORTED_FORMAT) | 415 | No |
| [`IDEMPOTENCY_BODY_MISMATCH`](#IDEMPOTENCY_BODY_MISMATCH) | 422 | No |
| [`RATE_LIMITED`](#RATE_LIMITED) | 429 | Yes, after `Retry-After` |
| [`TOO_MANY_ACTIVE_JOBS`](#TOO_MANY_ACTIVE_JOBS) | 429 | Yes, after `Retry-After` (30 s) |
| [`THROTTLED`](#THROTTLED) | 429 | Not in practice (`Retry-After` 3600 s) |
| [`INTERNAL_ERROR`](#INTERNAL_ERROR) | 500 | Yes, with backoff |
| [`SERVICE_UNAVAILABLE`](#SERVICE_UNAVAILABLE) | 503 | Yes, after `Retry-After` (30 s) |

## Request errors (400)

### INVALID_REQUEST

`400`, not retryable. The body or a query parameter is malformed, missing, or out of range: a missing `fileName`, an unknown `processingMode`, metadata over the limits, a bad `cursor`, `state` and `status` filters combined, `track` missing on a multi-track SRT request. `param` names the field. Fix it and send a new request.

### INVALID_URL

`400`, not retryable. A media URL (`url`, `audioUrl`, `audioTracks[].url`, `clips[].sourceUrl`) is not an absolute `http` or `https` URL. Send the full URL including the scheme.

### URL_NOT_ALLOWED

`400`, not retryable. The URL points to a private, loopback, link-local or cloud-metadata address, or an internal host name. We only fetch from the public internet. Use a publicly reachable URL, such as a presigned link to your object storage.

### INVALID_CALLBACK_URL

`400`, not retryable. `callbackUrl` (or the `url` of `POST /webhooks/test`) is not a public HTTPS URL. Use `https://` on a public host; plain HTTP and private addresses are refused.

## Authentication and account (401, 402, 403)

### UNAUTHORIZED

`401`, not retryable. The API key is missing, malformed, revoked, rotated or expired. Check that `FRAMEQUERY_API_KEY` holds a current `fq_live_` key and that you send it as `Authorization: Bearer <key>` or `X-API-Key: <key>`. After a rotation the old key stops working within about a minute. See [Authentication](https://www.framequery.com/docs/authentication).

### INSUFFICIENT_CREDITS

`402`, retry after topping up. Your available credits (balance minus holds for running jobs) do not cover even the 30-second minimum for this job. Buy credits on the [API keys page](https://www.framequery.com/profile/api-keys) and check `GET /quota`. The same code appears as a **job** error when the balance cannot cover the full file once its duration is known; that job is not charged.

### INSUFFICIENT_SCOPE

`403`. Reserved for scoped keys. Keys have no scopes during the beta, so the API does not return it today.

### ACCOUNT_SUSPENDED

`403`, not retryable. The account is suspended. Contact support@framequery.com.

## Not found (404)

### NOT_FOUND

`404`, not retryable. No such job, track or key for this account. Jobs of other accounts look exactly like missing ones, and an id that is not a UUID is also `404`. Check the id and that the key belongs to the account that created the job.

### RESULTS_UNAVAILABLE

`404`, never retry. The requested result does not exist and never will: the stage was skipped or failed, the audio track failed, or the job was cancelled, expired or failed before the stage ran. The body adds `state`, `stage`, `stageStatus`, `reason` (`processing_mode`, `audio_only_input`, `no_audio`, `stage_skipped`, `stage_failed`, `track_failed`, `job_cancelled`, `job_expired`, `job_failed`), and `errorCode` or `trackIndex` when relevant. Stop polling for this result. See [Results](https://www.framequery.com/docs/results#not-ready-versus-unavailable).

## Conflicts (409, 422)

### JOB_NOT_CANCELLABLE

`409`, not retryable. The job is already `completed`, `failed`, `cancelled` or `expired`. The body adds `jobId` and `state`. Nothing to do.

### IDEMPOTENCY_IN_PROGRESS

`409`, retryable. An earlier request with the same idempotency key is still running. Wait `Retry-After` (2 seconds) and retry with the same key and body; you will get the original response.

### RESULTS_NOT_READY

`409`, retryable. The stage is `pending` or `running` and may still complete. The body adds `state`, `stage` and `stageStatus`. Retry after `Retry-After` (15 seconds) or wait for the webhook.

### IDEMPOTENCY_BODY_MISMATCH

`422`, not retryable. The idempotency key was already used, within the last 24 hours, with a different request body (or the header and body keys differ). Use a new key for each distinct request.

## File problems (413, 415)

### FILE_TOO_LARGE

`413`, not retryable. `fileSizeBytes` is over the limit for the file's tier (20 GiB standard, 100 GiB camera RAW). The body adds `maxUploadBytes` and `formatTier`, and `param` is `fileSizeBytes`. Split the file, or [ask us](https://www.framequery.com/contact) about a higher limit.

### UNSUPPORTED_FORMAT

`415`, not retryable. The file type is blocked (executables and scripts), or it is a camera RAW format that is not enabled yet. Check `GET /formats`. See [Formats and limits](https://www.framequery.com/docs/formats).

## Rate limits (429)

### RATE_LIMITED

`429`, retryable. The account went over a per-minute limit (600 GETs, 30 job creations, 120 other requests), or a client IP went over 120 requests per minute on the keyless endpoints. Wait `Retry-After` seconds. `X-RateLimit-Reset` is when the window resets, as a Unix timestamp. See [Rate limits](https://www.framequery.com/docs/rate-limits).

### TOO_MANY_ACTIVE_JOBS

`429`, retryable. The account has too many unfinished jobs. The body adds `maxActiveJobs` and `activeJobs`. Wait `Retry-After` (30 seconds) or for running jobs to finish, then retry. [Ask us](https://www.framequery.com/contact) if you need a higher limit.

### THROTTLED

`429`. Job creation is temporarily restricted for this account, with `Retry-After: 3600`. The SDKs do not wait that long. Contact support@framequery.com if it persists.

## Server errors (500, 503)

### INTERNAL_ERROR

`500`, retry with backoff. Something failed on our side. Retry with exponential backoff and the same idempotency key, so a create that did succeed is replayed rather than duplicated. If it persists, send us the `requestId`.

### SERVICE_UNAVAILABLE

`503`, retryable. A dependency is briefly unavailable, for example your credit balance could not be verified. Retry after `Retry-After` (30 seconds).

## Job and stage error codes

These are not HTTP errors. They appear in a job's `error.code` when its `state` is `failed`, and in `stages.<stage>.error.code` for a stage that failed. A failed stage is never charged; other stages that completed are charged and stay readable.

| Code | Stage | Meaning | What to do |
|---|---|---|---|
| `VALIDATION_FAILED` | ingest | Unsupported or corrupt media, or a limit (such as duration) exceeded | Check the file plays and is within [limits](https://www.framequery.com/docs/formats#limits) |
| `INSUFFICIENT_CREDITS` | ingest | The balance could not cover the full file once its duration was known | Buy credits, then create a new job |
| `FETCH_FAILED` | ingest | The source URL could not be downloaded (unreachable, an HTTP error, an expired link or token) | Check the URL works from the public internet, then create a new job |
| `INGEST_FAILED` | ingest | The file could not be processed | Create a new job once; if it fails again, contact us with the `jobId` |
| `EXTERNAL_AUDIO_FAILED` | ingest | A separate audio file could not be processed | Check the audio file and its URL |
| `VISION_FAILED` | vision | Scene analysis failed | Resubmit with `processingMode: "vision"` |
| `CONTENT_BLOCKED` | vision | The content is not permitted by the acceptable-use policy | See our [terms](https://www.framequery.com/terms) |
| `TRANSCRIPT_FAILED` | transcript | Transcription failed | Resubmit with `processingMode: "transcript"` |
| `PROCESSING_FAILED` | any | Another failure | Resubmit; contact us if it repeats |
| `CANCELLED` | any | The stage was skipped because you cancelled the job | Nothing |
| `EXPIRED` | any | The job expired before the stage ran | Create a new job |

Resubmitting a single mode after a partial failure means you only pay for the stage you are missing.
