# Projects

> Create, change, start, stop and delete projects.

## The project resource

```json
{
  "data": {
    "id": "01m3k8jnrdw509457nhz7w81bq",
    "name": "Fishing Channel S01",
    "description": null,
    "buildVersion": "0.9.0-alpha1-2026-10-05T14-57Z-bf4a93",
    "status": "running",
    "files": { "total": 3, "pending": 1, "processing": 0, "done": 1, "failed": 1, "countedAt": "2026-09-28T06:15:00.000Z" },
    "startOnNewFiles": true,
    "config": {
      "sourceMedia": { "@type": "S3FolderSource" },
      "transcription": { … },
      "translations": [ … ],
      "outputs": [ { "@type": "S3FolderOutput", "items": [ … ] } ]
    },
    "storage": {
      "connection": null,
      "path": "s3://sw-vod-express/projects/141/01m3k8jnrdw509457nhz7w81bq/",
      "inputPath": "s3://sw-vod-express/projects/141/01m3k8jnrdw509457nhz7w81bq/input/",
      "outputPath": "s3://sw-vod-express/projects/141/01m3k8jnrdw509457nhz7w81bq/output/"
    },
    "meta": {
      "createdAt": "2026-09-28T05:42:35.534Z",
      "updatedAt": "2026-09-28T05:42:35.534Z",
      "createdBy": { "accountId": 141, "accountName": "Acme Media", "userId": null, "userEmail": null, "userFullName": null },
      "configLocked": false,
      "messages": []
    }
  }
}
```

- `id` (string): A lowercase ULID. It sorts by creation time.
- `name` (string): Up to 255 characters. Required on create. Names need not be unique.
- `description` (string | null): Up to 2000 characters.
- `buildVersion` (string): The processing build the project runs. Chosen at creation (your account's default unless you name one) and kept until you change it. See [Builds](/vod-express/configuration/overview#builds-and-the-schema).
- `status` ("running" | "stopped" | "error"): Whether the processing service is up. `error` means the last launch failed, or the service started and then stopped working (`pod` in `GET …/status`); `POST …/start` retries it.
- `files` (object | null): How many input files are in each state, as last counted (`countedAt`): the same counts as `meta` of [`GET …/files`](/vod-express/concepts/files), kept on the project so that a list of projects shows each one's progress without a call per project. A project with a service, or changed in the last day, is counted once a minute; an upload completed, a file deleted or retried, or a files listing through this API counts again at once, so a file copied in with the AWS CLI shows at the next minute. `null` until the first count. The project is finished when `pending` and `processing` are both 0.
- `startOnNewFiles` (boolean): Whether a new file in the input folder starts the project when it is stopped. `true` unless you turn it off. See [Starting again on a new file](#starting-again-on-a-new-file).
- `config` (object): The processing instructions, as you sent them, with the engines filled in where you left them out, and `sourceMedia` written in as `{"@type": "S3FolderSource"}` if you left it out. Its `sourceMedia` says where the media comes from: see [Inputs](/vod-express/concepts/inputs). See [Configure a project](/vod-express/configuration/overview).
- `storage` (object): Where the project's files are. Upload into `inputPath`; results appear in `outputPath`. `connection` is `null` for SyncWords' storage, where `path` is the project's folder (the one the [upload role](/vod-express/uploads/aws-role) reaches), or names the account's storage connection — [your own bucket](/vod-express/uploads/own-bucket) — whose two folders the project uses.
- `meta` (object): When the project was created and last changed, and who created it (`createdBy`: the account, and the user, null for an API key). `configLocked` is `true` while SyncWords support manages the project's configuration for you: see [Change](#change). `messages` lists what the config will do that you probably do not mean: see [Messages](#messages).

## Messages

A config can be valid and still waste money. The commonest case: a language is translated, or
dubbed, but no output carries it. Every language in `translations` is produced and
[billed](/vod-express/concepts/how-it-works#billing) for every file, whatever the outputs hold, so a Spanish
dub that no output has a dub track for costs a dub and delivers nothing.

That config is not refused — a project can be set up in steps, and a file can be
[retried](/vod-express/concepts/files#retry) once the output is added, without being billed again. It
is reported in `meta.messages` instead, on the create or change that stored it and on every read
after, until the config is fixed:

```json
"messages": [
  {
    "severity": "warning",
    "code": "unused_dub",
    "path": "/config/translations/0/ttsOptions",
    "message": "The es dub is in no output: no video or audio file has a dub track in es. It is still produced and billed for every file. Add a dub track in es to an output, or turn dubbing off for this translation.",
    "params": { "language": "es" }
  }
]
```

- `severity` ("info" | "warning" | "error"): How much it matters. `warning`: it costs money or loses output. More values may be added; treat one you do not know as `info`.
- `code` (string): Stable, one per situation: branch on it. The `message` may be reworded. | Code | When | What to change | |---|---|---| | `unused_translation` | No output carries the translation: no subtitle file or subtitle track in its language, and, if it is dubbed (`params.dubbed`), no dub track either. It is still produced and billed, and so is its dub. | Add the language to an output, or remove the translation. | | `unused_dub` | The translation's subtitles are in an output, but no video or audio file has a dub track in its language. The dub is still produced and billed. | Add a dub track in the language to an output, or remove `ttsOptions`. | | `dictionary_not_applied` | A dictionary, postprocessing dictionary or glossary of your account that the config names (`params.reference`, in `params.field`) was deleted (`params.reason`: `not_found`) or is no longer in the project's languages (`language`). The project processes without it. | Choose another one, or remove it. | More codes may be added.
- `path` (string | null): What it is about: a JSON pointer, the same as the keys of a 422's `errors`. Null when it is about the whole project.
- `message` (string): For people: what is wrong, what it costs, and what to change.
- `params` (object): The values `message` names, for a client that words it itself: `language`, and `dubbed` on `unused_translation`.

## Create

`POST /projects` takes `name`, `config`, and optionally `description`, `buildVersion`,
`startOnNewFiles`, `storage` and `options`:

```bash JSON
curl -s -X POST $API/projects -H "$AUTH" -H 'Content-Type: application/json' \
-d @project.json
```

```bash YAML
curl -s -X POST $API/projects -H "$AUTH" -H 'Content-Type: application/yaml' \
--data-binary @project.yml
```

The config is validated in full before anything is stored: against the build's schema, then
against rules the schema cannot express (see [Validation](/vod-express/configuration/overview#validation)). An
invalid config answers `422` and nothing is created.

The config's `sourceMedia` says where the project's media comes from. Left out, the project reads
its input folder (`{"@type": "S3FolderSource"}`); `{"@type": "RssSource", "url": "https://…"}`
reads an RSS feed instead, `{"@type": "MrssSource", …}` a Media RSS feed. Choose between the
folder and a feed on create: a change between the two is refused with `422` under
`/config/sourceMedia/@type` (a feed's `url` and its kind may change). See [Inputs](/vod-express/concepts/inputs). The project-level `input` field that
said this before is gone: sent, it is refused with `422` and
`The input field is not supported: where the media comes from is config.sourceMedia.` under
`errors.input`.

`startOnNewFiles` (default `true`) is stored with the project: whether a new file in the input
folder starts it when it is stopped. See [Starting again on a new file](#starting-again-on-a-new-file).

`storage` puts the project's input and output folders in [your own bucket](/vod-express/uploads/own-bucket):
`{"connectionId": "<a storage connection>", "inputPrefix": "dubbing/in/", "outputPrefix": "dubbing/out/"}`,
the prefixes optional. Left out, the account's default connection decides (`GET /account/settings`);
`{"connectionId": null}` is SyncWords' storage whatever the default. A project on your bucket reads a
folder and writes folders: no feed source, no feed outputs.

`options` are instructions for the create itself and are not stored:

| Option | Default | Meaning |
|---|---|---|
| `autoStart` | `true` | Start the processing service now. `false` creates the project `stopped`; start it with `POST /projects/{id}/start` once its files are uploaded. |

> **Note:**
> If the config is valid but the service fails to start, the project is still created, in
> `status: "error"`. The `422` then has `code: "service_start_failed"` and carries the project's
> `id`; call `POST /projects/{id}/start` to try again rather than creating another project:
>
> ```json
> {
>   "message": ["Failed to start the processing service"],
>   "code": "service_start_failed",
>   "id": "01m3k8jnrdw509457nhz7w81bq",
>   "reason": "The processing service could not be started. Start the project again, or contact support if it keeps failing."
> }
> ```

### Repeating a create

`POST /projects` creates a project every time. Send an `Idempotency-Key` with it, and a repeat of
the call with the same key and body answers what the first call did instead of creating a second
project; the answer has `Idempotency-Replayed: true`. Use a new key, such as a UUID, for each
project you mean to create. See [Repeating a create](/vod-express/guides/errors#repeating-a-create).

## List and find

```bash
curl -s -H "$AUTH" "$API/projects?search=fishing&status=running&perPage=50&page=1"
```

| Parameter | Meaning |
|---|---|
| `name` | The whole name, exactly, case included. |
| `search` | Part of the name, case-insensitive. |
| `status` | `running`, `stopped` or `error`; or by what the files reached, as `files` last counted them: `completed` (stopped, nothing left to do and at least one file `done`) or `failed` (in `error`, or stopped with nothing left to do and no file `done`). |
| `perPage` | 1 to 100, default 25. |
| `page` | From 1. |

Projects come newest first. The answer carries `links` (`first`, `last`, `prev`, `next`) and
`meta` (`current_page`, `last_page`, `per_page`, `total`, …). Follow `links.next` until it is
`null`.

Names need not be unique, and `POST /projects` creates a project every time. So an integration
that keeps one project per configuration finds it by name first (an `Idempotency-Key` protects
one create for 24 hours; the name lasts):

```bash
curl -s -G -H "$AUTH" "$API/projects" --data-urlencode 'name=Fishing Channel S01' | jq -r '.data[0].id // empty'
```

## Change

`PATCH /projects/{id}` (or `PUT`, which behaves the same) takes any of `name`, `description`,
`buildVersion`, `config` and `startOnNewFiles`. **A field you send replaces the stored one**, so to change one
output you send the whole `config` with that output changed:

```bash
# Read, edit, write back
curl -s -H "$AUTH" $API/projects/$PROJECT | jq '.data.config' > config.json
# … edit config.json …
jq -n --slurpfile c config.json '{config: $c[0]}' \
  | curl -s -X PATCH $API/projects/$PROJECT -H "$AUTH" -H 'Content-Type: application/json' -d @-
```

Changing `name`, `description` or `startOnNewFiles` has no effect on processing, and restarts
nothing. Changing `config` or
`buildVersion` of a **running** project restarts its service with the new instructions:

- files already `done` are **not** reprocessed; [retry](/vod-express/concepts/files#retry) the ones you want
  again;
- a file being processed at that moment is started again under the new config.

If the restarted service does not start, **the change is stored anyway**: the old service is
already gone. The answer is then `422` with `code: "service_start_failed"` and the project's `id`,
the project reads `error`, and `POST /projects/{id}/start` retries with the new config. A `422`
with `validation_failed` is the other kind: the config was refused, and nothing was stored.

While `meta.configLocked` is `true`, SyncWords support manages the project's configuration for
you, and a call that would change `config` or `buildVersion` answers `409` with
`code: "project_config_locked"`; nothing is stored. `name` and `description` can still be
changed, and a `config` equal to the stored one (as `GET /projects/{id}` shows it, or as you first
sent it) is accepted, so a client that always sends its whole project keeps working. Contact
[support](/vod-express/support) to change the configuration.

## The processing service

A project runs one processing service while its `status` is `running`. That is the project's
`status`, from `GET /projects/{id}`: `running`, `stopped` or `error`. `GET /projects/{id}/status`
reports the service instead, and names its own `status` field for it: `ACTIVE`, `NOT_RUNNING` or
`ERROR`, with the project's status beside it as `projectStatus`.

| Call | Effect |
|---|---|
| `POST /projects/{id}/start` | Starts the service. On a service that is already healthy it does nothing, so it is safe to call after every upload. |
| `POST /projects/{id}/stop` | Stops it, and answers once the service is gone, so a start straight after never runs two. Files are untouched; files not yet processed wait, and a file being processed is processed again from the start on the next run. |
| `GET /projects/{id}/status` | What the service is doing now. |

```json GET /projects/{id}/status
{
  "data": {
    "status": "ACTIVE",
    "orchestratorStatus": "ready",
    "serviceId": "vod-01m3k8jnrdw509457nhz7w81bq-nhx79dps",
    "launchedAt": "2026-09-28T05:43:02.000Z",
    "version": "0.9.0-alpha1-2026-10-05T14-57Z-bf4a93",
    "projectStatus": "running",
    "lastError": null,
    "pod": { "status": "ready", "message": null, "at": "2026-09-28T05:43:04.969Z" }
  }
}
```

`status` is `ACTIVE` (up), `NOT_RUNNING` (stopped) or `ERROR` (failed to launch, or launched and
then stopped working). On `ERROR`, `POST …/start` replaces the service.

### Auto-stop

A running service holds compute even while it waits, so a project **stops itself** in two cases:

| When | Why | `project.stopped` reason |
|---|---|---|
| Every file in the input folder has finished (`done` or `failed`) and nothing has happened for **30 minutes**: no file finished, no start | Nothing left to do | `idle` |
| Its service has read `failed` for **2 hours** | It would process nothing, and `project.error` was already sent | `idle` |

It never stops itself:

- with an **empty input folder**: stopping that one is your call;
- for being idle when it **reads a [feed](/vod-express/concepts/inputs#feeds)**: with nothing left
  to process it is waiting for the feed's next item, which only a running service fetches. Stop it
  with `POST /projects/{id}/stop` when you no longer want the feed read.

Stopping touches nothing in the folders, and a stopped project costs nothing.

### Starting again on a new file

`startOnNewFiles` (default `true`) decides whether a new file in the input folder starts a stopped
project. Set it on [create](#create), change it with [`PATCH`](#change) at any time (it takes effect
at once and restarts nothing), and read it on the project.

| The project is stopped because | `startOnNewFiles: true` | `startOnNewFiles: false` |
|---|---|---|
| It stopped itself after its last file | **Starts**: at once when an API upload in parts completes, within a minute for any other file (the AWS CLI, an SDK, a one-`PUT` upload) | Waits |
| You stopped it (`POST …/stop`), or its service failed for 2 hours | Starts when an API upload in parts completes; any other file waits | Waits |
| It was created with `autoStart: false` and never started | Waits, so every file is in before the first start | Waits |
| It is in `error` | Waits | Waits |

- **"A new file" means one not processed yet.** A file copied over a finished one of the same name
  does not count (see [Replace](/vod-express/concepts/files#replace)).
- **Retrying a file starts a stopped project that ran before, whatever the setting**: a retry is
  you asking for that file.
- **A file that waits stays `pending`** until `POST /projects/{id}/start`. The call is harmless on
  a running project, so an upload routine can always end with it.
- Completing an API upload answers the project's state afterwards in `projectStatus`.

## Delete

`DELETE /projects/{id}` stops the service and deletes the project. **The files in its folders
are kept**, so download what you need first. It answers `204`.

## Lifecycle errors

Starting, stopping, changing and deleting a running project talk to the processing platform. If it
does not answer, the call fails without changing anything:

| Status | Meaning |
|---|---|
| `502` | `service_stop_failed`: the running service could not be stopped. Nothing was changed; try again. |
| `503` | `platform_unavailable` (start): the current service's state could not be read, so no second one was started; try again. |
| `422` | `service_start_failed` (create, change, start): the service did not start; `lastError` in `GET …/status` says so. On a change, the change was stored. |
