# Errors

> The error body, every error code, and the validation messages you are likely to meet.

## The error body

Every error is JSON with a `message` and a `code`. Validation errors (`422`) add `errors`, every
problem at once:

```json
{
  "message": "The colour field is not supported.",
  "code": "validation_failed",
  "errors": { "colour": ["The colour field is not supported."] }
}
```

- **`code`** is for your program: one per situation, and it does not change. Branch on it, never
  on the wording of `message`.
- **`message`** is for people: show or log it. It is a string or, on some statuses, an array of
  one string: read it as `Array.isArray(m) ? m[0] : m`.
- **`errors`** maps a field to its messages. Outside `config` the key is the field name (`name`,
  `status`, `parts.0.etag`). Inside `config` it is a **JSON pointer** into your request:
  `/config/translations/0/language` is the `language` of the first translation.

Send `Accept: application/json` to be sure an error comes back as JSON.

## Error codes

| `code` | Status | Meaning | What to do |
|---|---|---|---|
| `bad_request` | `400` | The request cannot be acted on as sent. | Read `message`. |
| `invalid_output_path` | `400` | `path` is not a file under `output/`. | Use a `path` from `GET …/outputs`. |
| `invalid_idempotency_key` | `400` | `Idempotency-Key` is not 1 to 255 visible ASCII characters. | Use a UUID. [Repeating a create](#repeating-a-create). |
| `account_required` | `400` | SyncWords staff only: the account to act for is not named. | — |
| `unauthenticated` | `401` | No token, or an expired or unknown one. | Create a new token and repeat the call. |
| `invalid_credentials` | `401` | `POST /token/create` with a key pair that matches no account. | Check the pair; repeating it does not help. |
| `staff_only` | `401` | A call only SyncWords staff may make. | — |
| `forbidden` | `403` | The token may not make this call. | — |
| `vod_express_not_enabled` | `403` | VOD Express is not available to the account. | Ask [support](/vod-express/support). |
| `account_restricted` | `403` | The account is restricted: its access was revoked, or its trial expired or ran out. `message` says which. | Ask [support](/vod-express/support). |
| `not_found` | `404` | No such route or resource. | Check the path. |
| `project_not_found` | `404` | No such project in your account. | Check the id, and the token's account. |
| `file_not_found` | `404` | No input file of that name. | Check the name and its [encoding](/vod-express/concepts/files#subfolders). |
| `file_meta_not_found` | `404` | Nothing recorded about the file yet. | Wait: processing has not reached it. |
| `output_not_found` | `404` | No such output. | It may not be written yet: see `available` in `GET …/files`. |
| `feed_not_found` | `404` | No such output feed or feed media file: the project publishes no feed in that language, or the feed has not been written yet. | Check the `MrssOutput` languages in the config; a new feed appears once the source feed has been read. [Media RSS feeds](/vod-express/concepts/inputs#feeds). |
| `upload_not_found` | `404` | The upload was completed, aborted, expired, or never started. | Start a new upload. |
| `webhook_not_found` | `404` | No such webhook in your account. | Check the id: `GET /webhooks`. |
| `webhook_delivery_not_found` | `404` | The webhook has no such delivery. | Check the event id: `GET /webhooks/{id}/deliveries`; deliveries are kept 7 days. |
| `aws_access_not_provisioned` | `404` | No upload role yet. | `PUT /account/aws-access`. |
| `storage_connection_not_found` | `404` | No such storage connection in your account. | Check the id: `GET /account/storage-connections`. |
| `method_not_allowed` | `405` | The path exists, but not with this method. | Check the method. |
| `conflict` | `409` | The request conflicts with the resource's current state. | Read `message`. |
| `file_exists` | `409` | An API upload of a name already in `input/`, without `overwrite`. | Set `overwrite: true`, or choose another name. |
| `project_reads_feed` | `409` | An upload to a project whose `sourceMedia` is a feed: the feed is its only source. | Add the media to the feed, or create a folder project. |
| `project_reads_folder` | `409` | `GET …/source-feed` of a project that reads its input folder: it has no source feed. | — |
| `file_processing` | `409` | Replacing or deleting a file that the running project may be processing: it is neither `done` nor `failed` yet. | Wait until it is, or stop the project, then repeat the call. |
| `project_config_locked` | `409` | SyncWords support manages the project's configuration for now (`meta.configLocked` is `true`): the call would change `config` or `buildVersion`. `name` and `description` can still be changed. | Contact [support](/vod-express/support). |
| `idempotency_in_progress` | `409` | The first `POST /projects` with this `Idempotency-Key` has not finished. | Repeat the call after `Retry-After` seconds. |
| `storage_connection_in_use` | `409` | Deleting a storage connection a project of your account uses. | Move the project to other storage (`PATCH /projects/{id}` with `storage`), then delete. |
| `payload_too_large` | `413` | The body is larger than 2 MB. | Send less: a config, or up to 10,000 parts, fits. [Limits](/vod-express/guides/limits#request-bodies). |
| `unsupported_media_type` | `415` | The body is neither JSON nor YAML. | Set `Content-Type: application/json` (or `application/yaml`). |
| `validation_failed` | `422` | The request is invalid. | Fix every field in `errors`. |
| `service_start_failed` | `422` | The config is valid, but the processing service did not start. | `POST /projects/{id}/start`; the project exists. |
| `idempotency_key_reused` | `422` | The `Idempotency-Key` was used with another body. | Use a new key for a new project. |
| `webhook_limit_reached` | `422` | The account has 10 webhooks, the most it may have. | Delete one first. |
| `storage_failed` | `422` | The project's folders could not be created; nothing was stored. On your own bucket: the keys could not write to it. | Repeat the call; on your own bucket, check the key's permissions. |
| `storage_connection_check_failed` | `422` | The bucket could not be listed with the keys sent: a wrong key, bucket or endpoint, or no permission. `message` carries the store's own words. | Fix the keys, bucket or endpoint. [Your own bucket](/vod-express/uploads/own-bucket). |
| `storage_connection_folder_only` | `422` | A project on your own bucket with a feed source or a feed output. | Use SyncWords' storage for feed projects, or folders on your bucket. |
| `storage_prefix_taken` | `422` | Another project of your account watches this input folder, or one inside it, on the same connection. | Choose another `inputPrefix`. |
| `build_without_storage_connections` | `422` | The build named cannot read your own bucket. | Choose a later build from `GET /build-versions`. |
| `build_has_no_schema` | `422` | The named build cannot be used. | Choose another from `GET /build-versions`. |
| `too_many_requests` | `429` | More than 1000 requests in a minute from your account, 10,000 parts signed, or 60 refusals of one token or key pair. | Wait `Retry-After` seconds. [Limits](/vod-express/guides/limits#rate-limit). |
| `internal_error` | `500` | An unexpected error on our side. | Repeat once; then contact [support](/vod-express/support). |
| `bad_gateway` | `502` | A service the API depends on failed. | Repeat the call. |
| `service_stop_failed` | `502` | A running service could not be stopped; nothing was changed. | Repeat the call. |
| `aws_access_failed` | `502` | AWS refused a step of creating the upload role. | Repeat the call. |
| `platform_unavailable` | `503` | The processing platform could not be reached; nothing was changed. | Repeat the call. |
| `service_unavailable` | `503` | The API cannot serve this call right now. | Repeat later. |
| `build_list_unavailable` | `503` | The list of builds, or a build's schema, cannot be fetched right now; nothing was changed. | Repeat the call. |

New codes may be added; treat an unknown one by its status.

## Config validation

Two kinds of message come back under `/config/…`: the schema's, about shape, and the API's, about
meaning.

### Schema messages

| Message | Example cause |
|---|---|
| `The ext field is required.` | `/config/outputs/0/items/0/ext` is missing. |
| `The property model is not supported here.` | A field this kind of object does not have: a typo, or an option of another engine (`model` on DeepL). |
| `Invalid value. Allowed values: keep, remove.` | A value outside the allowed list (`filterProfanity: "hide"`), or an unknown `@type` (`engine: {"@type": "ChatGPT"}` lists the translation engines). |
| `The value must be of type array.` | A wrong type (`translations: "es"`). A field that also takes null says `string or null`. |
| `The value must be less than or equal to 2.` | Out of range (`ttsOptions.volume: 3`). |

### Rule messages

These are checked once the config matches the schema, so fixing schema errors may bring up rule
errors on the next attempt.

| Message | Fix |
|---|---|
| `Language 'xx' is not supported by engine 'DeepL'` | Choose another engine or language: `GET /enums/languages`. |
| `The dub track language must be a dubbed translation: es, ru.` | A dub track (`/config/outputs/0/items/0/dubTracks/1/language`) names a language without `ttsOptions`. Dub it, or use a subtitle output. |
| `No translation has ttsOptions, so there is no dub to add: use useOriginalTracks for the input’s audio.` | As above, with no dub at all. For the input's own audio, set `useOriginalTracks: true`. |
| `An audio file needs its audio track: one dub track, or useOriginalTracks for the input’s audio.` | An `AudioFile` with neither, under `…/dubTracks`. Add one dub track or `useOriginalTracks: true`. |
| `An audio file holds exactly one audio track: the input’s audio (useOriginalTracks) or one dub track, not both.` | Keep one of them, and write the other into a second file. |
| `An audio file holds exactly one audio track: list one dub track.` | Write one audio file per dub. |
| `The input’s audio is copied as it is, which an mp3 file cannot hold: use ipod (m4a) or matroska, or a dub track.` (also `a wav file`) | Under `…/useOriginalTracks`. Use `containerFormat: "ipod"` with `ext: "m4a"`, or `matroska`. |
| `The name template must contain ${name}, or every input would write the same file.` | Add `${name}` to `nameTemplate`. |
| `The name template uses ${lang}, which a VideoFile does not have: ${ext}, ${name}.` | Only `SubtitleFile` has `${lang}`. |
| `The language must be the transcription language or a translation language: en, es.` | A subtitle file, or a video's subtitle track (`…/subTracks/0/language`), in a language that is not produced. Add the translation. |
| `The ext must not be blank.` (also `format`, and `The name template must not be blank.`) | Fill it in. A blank `SubtitleFile` template is taken as the default instead. |
| `The name template must render a relative path: …` | The name must stay inside the project's `output/` folder: no leading `/`, `//`, `.` or `..` segment, backslash or control character. |
| `The options must be an object.` | Caption `options` are an object of renderer settings. |
| `The format is set on the entry itself: leave options.format out, or make it SRT.` | Remove `format` from `options`; set the entry's `format`. |
| `The dubs es and es-MX are the same language (es)…` | A project dubs each language once: dub one of them, and keep the other as a subtitle translation if needed. |
| `The name template renders the same file name as outputs/0/items/0 (x.es.mp4).` | Two items of one destination write the same file. Give one of them another template. |
| `A project has one output folder, which outputs/0 is already: list these items there.` | Move the items into the first `S3FolderOutput`. |
| `An output needs at least one item.` | Add an item to the destination's `items`, or remove the destination. |
| `A feed output re-publishes the source feed: an MrssOutput needs an MrssSource as the sourceMedia.` (also `an RssOutput needs an RssSource`) | A feed output in a folder project, or of the other kind than the source. Use an `S3FolderOutput`, or the output of the source's kind. [Feeds](/vod-express/concepts/inputs#feeds). |
| `A feed output needs exactly one video or audio file: the one each feed item links to.` | Keep one `VideoFile` or `AudioFile` in the feed output's `items`. |
| `outputs/1 is already the es feed.` | One feed output per language. |
| `A project’s source cannot change between a folder and a feed: create another project.` | A `PUT`/`PATCH` whose `sourceMedia` is the other type, under `/config/sourceMedia/@type`. Create a new project for the other source. |
| `The URL must be an https:// address.` (also `…must not contain a user name or password.`, `…does not resolve to a public address, so the feed cannot be fetched.`) | The `sourceMedia` feed URL, under `/config/sourceMedia/url`. |

## Upload errors

| `code` | Status | Message (in `errors.name` or `errors.parts` on a `422`) |
|---|---|---|
| `validation_failed` | `422` | `The name must end in one of: .mp4, .mov, …` |
| `validation_failed` | `422` | `The name must be a file name, or a path of folders and a file name separated by /.` |
| `validation_failed` | `422` | `The name must not contain "${".` Output names are made by putting the name into `${name}`, and a `${` left over fails the file. |
| `validation_failed` | `422` | `x.mp4 is already in the project and its output x.es.mp4 would be overwritten by this file's. Rename the file.` |
| `file_exists` | `409` | `A file with this name is already in the project. Send overwrite: true to replace it.` |
| `file_processing` | `409` | `The file may be being processed. Wait until it is done or failed, or stop the project, then try again.` |
| `project_reads_feed` | `409` | `This project reads its media from a feed; files cannot be uploaded to it.` |
| `upload_not_found` | `404` | `Upload not found.`: the upload was completed, aborted or expired. |

## Retrying safely

| Call | Safe to repeat? |
|---|---|
| Every `GET` | Yes |
| `POST /projects/{id}/start`, `…/stop`, `…/retry` | Yes: they converge on the same state. |
| `PUT` / `PATCH /projects/{id}` | Yes, with the same body. |
| `DELETE` of a project, file or upload | Yes; a second call answers `404` (or `204` for an upload). |
| `POST /projects` | **With an `Idempotency-Key`**: a repeat answers what the first call did. Without one, each call creates a project. [Repeating a create](#repeating-a-create). |
| `POST …/uploads/{uploadId}/complete` | Yes, with the same body: once the file is in `input/`, a repeat answers `200` again. |
| `POST …/uploads` | Starts a new upload each time; abort the ones you do not complete. An upload you never complete creates nothing and is discarded after 3 days. A `PUT` to its `url` is repeatable: the last one wins. |

### Repeating a create

Send an `Idempotency-Key` header with `POST /projects`: any string of 1 to 255 visible ASCII
characters, unique per project you mean to create. A UUID is a good one. If the call times out or
the connection drops, repeat it with the **same key and the same body**:

```bash
KEY=$(uuidgen)
curl -s -X POST "$API/projects" -H "$AUTH" -H "Idempotency-Key: $KEY" \
  -H 'Content-Type: application/json' -d @project.json
# … no answer? Repeat the very same call:
curl -s -i -X POST "$API/projects" -H "$AUTH" -H "Idempotency-Key: $KEY" \
  -H 'Content-Type: application/json' -d @project.json
```

| The repeat finds | It answers |
|---|---|
| The first call finished | The first call's answer, status included, with `Idempotency-Replayed: true`. No second project. The project is shown as it was then; `GET /projects/{id}` reads it now. |
| The first call still running | `409`, `code: "idempotency_in_progress"`, and `Retry-After`. Repeat the call again after that many seconds. |
| The key used with another body | `422`, `code: "idempotency_key_reused"`. |
| The first call created nothing | As if the key were new: the call runs. |

What is stored is an answer that created a project: the `201`, and a `422` with
`code: "service_start_failed"` (the project exists; start it with `POST /projects/{id}/start`). A
call refused before anything was created (`validation_failed`, `build_has_no_schema`, …) and a
`5xx` leave the key unused, so you can correct the body and send it with the same key.

Keys belong to your account, and are kept for **24 hours** from their first use. The body is
compared by content: the same project sent as JSON or YAML, or with its fields in another order,
is the same request.

### Finding a project by name

An `Idempotency-Key` protects one create. To keep one project per configuration across runs, days
apart, look the name up before creating it:

```bash
# Create a project only if none has this name yet
NAME='Fishing Channel S01'
ID=$(curl -s -G -H "$AUTH" "$API/projects" --data-urlencode "name=$NAME" | jq -r '.data[0].id // empty')
[ -z "$ID" ] && ID=$(curl -s -X POST "$API/projects" -H "$AUTH" -H 'Content-Type: application/json' \
  -d "$(jq -n --arg name "$NAME" --slurpfile c config.json '{name: $name, config: $c[0]}')" | jq -r .data.id)
```
