# Update a project

`PATCH https://api-vod-express.syncwords.com/v1/projects/{id}`

Update a project

Every field is optional, and a field sent replaces the stored one: to change one output, send the whole `config`. Changing `config` or `buildVersion` of a running project restarts its processing service with the new instructions; files already done are not processed again. If the running service cannot be stopped, the answer is 502 and nothing is stored. If the restarted service does not start, the change **is** stored and the answer is 422 `service_start_failed`: the project reads `error` until `POST …/start` succeeds.

**Guide:** [Projects](/vod-express/concepts/projects)

## Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The project id, a lowercase ULID. |

## Request body

Example:

```json
{
  "name": "Fishing Channel S01 — Spanish"
}
```

Schema:

```json
{
  "$ref": "#/components/schemas/ProjectUpdate"
}
```

## Responses

### 200

OK

Example:

```json
{
  "data": {
    "id": "01m3k8jnrdw509457nhz7w81bq",
    "name": "Fishing Channel S01",
    "description": null,
    "buildVersion": "0.9.0-alpha1-2026-10-20T14-10Z-3f05aa",
    "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": {
        "@type": "Speechmatics",
        "language": "en"
      },
      "translations": [
        {
          "language": "es",
          "engine": {
            "@type": "Gemini"
          },
          "ttsOptions": {
            "provider": {
              "@type": "Deepdub",
              "voice": "auto"
            }
          }
        }
      ],
      "outputs": [
        {
          "@type": "S3FolderOutput",
          "items": [
            {
              "@type": "VideoFile",
              "ext": "mp4",
              "nameTemplate": "${name}.es.${ext}",
              "useOriginalTracks": true,
              "dubTracks": [
                {
                  "language": "es",
                  "title": "Spanish"
                }
              ]
            }
          ]
        }
      ]
    },
    "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/",
      "inputPrefix": null,
      "outputPrefix": null
    },
    "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": []
    }
  }
}
```

Schema:

```json
{
  "type": "object",
  "required": [
    "data"
  ],
  "properties": {
    "data": {
      "$ref": "#/components/schemas/Project"
    }
  }
}
```

### 401

No token, or an expired or unknown one: `unauthenticated`.

Example:

```json
{
  "message": "Unauthenticated.",
  "code": "unauthenticated"
}
```

Schema:

```json
{
  "$ref": "#/components/schemas/Error"
}
```

### 403

VOD Express is not available to the account: switched off for it (`vod_express_not_enabled`), or the account is restricted — its access revoked, its trial expired or used up (`account_restricted`, with the reason in `message`).

Example:

```json
{
  "message": [
    "VOD Express is not enabled for your account."
  ],
  "code": "vod_express_not_enabled"
}
```

Schema:

```json
{
  "$ref": "#/components/schemas/Error"
}
```

### 404

No such project in your account: `project_not_found`.

Example:

```json
{
  "message": "Project not found.",
  "code": "project_not_found"
}
```

Schema:

```json
{
  "$ref": "#/components/schemas/Error"
}
```

### 409

SyncWords support manages the project’s configuration for now (`meta.configLocked` is `true`), and the call would change `config` or `buildVersion`: `project_config_locked`. Nothing is stored. `name` and `description` can still be changed, and a `config` equal to the stored one is accepted.

Example:

```json
{
  "message": [
    "The project's configuration is managed by SyncWords support and cannot be changed right now."
  ],
  "code": "project_config_locked"
}
```

Schema:

```json
{
  "$ref": "#/components/schemas/Error"
}
```

### 413

The body is over 2 MB: `payload_too_large`.

Example:

```json
{
  "message": [
    "The request body is too large."
  ],
  "code": "payload_too_large"
}
```

Schema:

```json
{
  "$ref": "#/components/schemas/Error"
}
```

### 415

The body is neither JSON nor YAML: `unsupported_media_type`.

Example:

```json
{
  "message": [
    "Send the body as JSON (Content-Type: application/json) or YAML (Content-Type: application/yaml)."
  ],
  "code": "unsupported_media_type"
}
```

Schema:

```json
{
  "$ref": "#/components/schemas/Error"
}
```

### 422

Invalid (`validation_failed`): nothing is stored. Or the change was stored and the restarted service did not start (`service_start_failed`): the body carries the project’s `id`, the project reads `error`, and `POST …/start` retries it with the new config.

Example:

```json
{
  "code": "validation_failed",
  "message": "No translation has ttsOptions, so there is no dub to add: use useOriginalTracks for the input’s audio. (and 2 more errors)",
  "errors": {
    "/config/outputs/0/items/0/dubTracks/0/language": [
      "No translation has ttsOptions, so there is no dub to add: use useOriginalTracks for the input’s audio."
    ],
    "/config/outputs/0/items/0/nameTemplate": [
      "The name template must contain ${name}, or every input would write the same file."
    ],
    "/config/translations/0/language": [
      "Language 'xx' is not supported by engine 'DeepL'"
    ]
  }
}
```

Schema:

```json
{
  "$ref": "#/components/schemas/Error"
}
```

### 429

Rate limited: `too_many_requests`. Wait the seconds `Retry-After` says, then repeat the call.

Example:

```json
{
  "message": [
    "Too Many Attempts."
  ],
  "code": "too_many_requests"
}
```

Schema:

```json
{
  "$ref": "#/components/schemas/Error"
}
```

### 502

The running processing service could not be stopped: `service_stop_failed`. Nothing was changed; repeat the call.

Example:

```json
{
  "message": [
    "Failed to stop the running processing service"
  ],
  "code": "service_stop_failed"
}
```

Schema:

```json
{
  "$ref": "#/components/schemas/Error"
}
```

## Schemas

### ProjectUpdate

`#/components/schemas/ProjectUpdate`

```json
{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 255
    },
    "description": {
      "type": [
        "string",
        "null"
      ],
      "maxLength": 2000
    },
    "buildVersion": {
      "type": "string"
    },
    "config": {
      "$ref": "#/components/schemas/Config",
      "description": "The processing instructions, validated against the schema of the project’s build. Shown here: build `0.9.0-alpha1-2026-10-20T14-10Z-3f05aa`; `GET /schema` has any build’s."
    },
    "startOnNewFiles": {
      "type": "boolean",
      "description": "Whether a new file starts a stopped project; see `startOnNewFiles` on create."
    },
    "storage": {
      "$ref": "#/components/schemas/ProjectStorageWrite"
    }
  }
}
```

### ProjectStorageWrite

`#/components/schemas/ProjectStorageWrite`

```json
{
  "type": "object",
  "required": [
    "connectionId"
  ],
  "additionalProperties": false,
  "description": "Where the project keeps its input and output folders: one of your storage connections — your own bucket — with a folder for each, or `connectionId: null` for SyncWords’ storage. Left out on create, your account’s default decides; left out on a change, nothing moves. A project on your own bucket reads its input folder and writes its output folder: no feed source, no feed outputs.",
  "properties": {
    "connectionId": {
      "type": [
        "string",
        "null"
      ],
      "description": "A connection from `GET /account/storage-connections`, or null."
    },
    "inputPrefix": {
      "type": "string",
      "maxLength": 512,
      "description": "The folder in the bucket the service watches for input files (`dubbing/in/`; a trailing slash is added). Default: `vod-express/<project id>/input/`. No other project of your account may watch this folder, or one inside it, on the same connection."
    },
    "outputPrefix": {
      "type": "string",
      "maxLength": 512,
      "description": "The folder results are written to. Default: `vod-express/<project id>/output/`. May not be the input folder or inside it."
    }
  }
}
```

### Project

`#/components/schemas/Project`

```json
{
  "type": "object",
  "required": [
    "id",
    "name",
    "description",
    "buildVersion",
    "status",
    "files",
    "startOnNewFiles",
    "config",
    "storage",
    "meta"
  ],
  "properties": {
    "id": {
      "type": "string",
      "description": "A lowercase ULID; ids sort by creation time."
    },
    "name": {
      "type": "string"
    },
    "description": {
      "type": [
        "string",
        "null"
      ]
    },
    "buildVersion": {
      "type": "string",
      "description": "The build the project runs on, kept until you change it."
    },
    "status": {
      "enum": [
        "running",
        "stopped",
        "error"
      ],
      "description": "Whether the processing service is up. `error`: the last start failed, or the service stopped working (`pod` in `GET …/status`); `POST …/start` retries it."
    },
    "files": {
      "type": [
        "object",
        "null"
      ],
      "required": [
        "total",
        "pending",
        "processing",
        "done",
        "failed",
        "countedAt"
      ],
      "description": "How many input files are in each state, as last counted: the counts of `GET …/files`’ `meta`, kept on the project so a list can show every project’s progress. A project with a service, or changed in the last day, is counted once a minute; every change made through this API (an upload completed, a file deleted or retried, a files listing) counts again at once. A file copied in with the AWS CLI shows at the next minute. `null` until the first count. A project is finished when `pending` and `processing` are both 0.",
      "properties": {
        "total": {
          "type": "integer"
        },
        "pending": {
          "type": "integer"
        },
        "processing": {
          "type": "integer"
        },
        "done": {
          "type": "integer"
        },
        "failed": {
          "type": "integer"
        },
        "countedAt": {
          "type": "string",
          "format": "date-time",
          "description": "When they were counted."
        }
      }
    },
    "startOnNewFiles": {
      "type": "boolean",
      "description": "When the project is `stopped` and a new file lands in its input folder, start it: at once when an API upload completes, and within a minute when the project stopped itself after its last file and the file came another way (the AWS CLI, an SDK, a one-`PUT` upload). False: new files wait for `POST …/start`. Retrying a file starts the project either way. See Auto-stop."
    },
    "config": {
      "$ref": "#/components/schemas/Config",
      "description": "The processing instructions, validated against the schema of the project’s build. Shown here: build `0.9.0-alpha1-2026-10-20T14-10Z-3f05aa`; `GET /schema` has any build’s."
    },
    "storage": {
      "type": "object",
      "required": [
        "connection",
        "path",
        "inputPath",
        "outputPath",
        "inputPrefix",
        "outputPrefix"
      ],
      "description": "Where the project’s files are: SyncWords’ storage (`connection` null; the folder the upload role reaches), or your own bucket through a storage connection, with the project’s two folders in it.",
      "properties": {
        "connection": {
          "type": [
            "object",
            "null"
          ],
          "required": [
            "id",
            "name",
            "bucket",
            "endpoint"
          ],
          "properties": {
            "id": {
              "type": "string"
            },
            "name": {
              "type": "string"
            },
            "bucket": {
              "type": "string"
            },
            "endpoint": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        },
        "path": {
          "type": "string",
          "description": "The project’s folder, or the folder of your bucket both others share."
        },
        "inputPath": {
          "type": "string",
          "description": "Upload here."
        },
        "outputPath": {
          "type": "string",
          "description": "Outputs appear here."
        },
        "inputPrefix": {
          "type": [
            "string",
            "null"
          ],
          "description": "The input folder in your bucket, as `storage` takes it; null for SyncWords’ storage."
        },
        "outputPrefix": {
          "type": [
            "string",
            "null"
          ]
        }
      }
    },
    "meta": {
      "type": "object",
      "required": [
        "createdAt",
        "updatedAt",
        "createdBy",
        "configLocked",
        "messages"
      ],
      "properties": {
        "createdAt": {
          "type": "string",
          "format": "date-time"
        },
        "updatedAt": {
          "type": "string",
          "format": "date-time"
        },
        "createdBy": {
          "type": "object",
          "required": [
            "accountId",
            "accountName",
            "userId",
            "userEmail",
            "userFullName"
          ],
          "description": "The owning account, and the user who created the project; null fields for an organization token, or a deleted account or user.",
          "properties": {
            "accountId": {
              "type": "integer"
            },
            "accountName": {
              "type": [
                "string",
                "null"
              ]
            },
            "userId": {
              "type": [
                "integer",
                "null"
              ]
            },
            "userEmail": {
              "type": [
                "string",
                "null"
              ]
            },
            "userFullName": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        },
        "configLocked": {
          "type": "boolean",
          "description": "True while SyncWords support manages the project’s configuration for you: `config` and `buildVersion` cannot be changed (`409 project_config_locked`) until support hands it back. `name` and `description` can be changed as usual."
        },
        "messages": {
          "type": "array",
          "items": {
            "$ref": "#/components/schemas/Message"
          },
          "description": "What the config will do that you probably do not mean, such as a dub no output carries. Empty when there is nothing to say."
        }
      }
    }
  }
}
```

### Message

`#/components/schemas/Message`

```json
{
  "type": "object",
  "required": [
    "severity",
    "code",
    "path",
    "message"
  ],
  "description": "Something worth changing about a project that was accepted: nothing is refused for it. Worked out from the stored config on every answer, so it shows on a read too, and goes away once the config is fixed.",
  "properties": {
    "severity": {
      "type": "string",
      "enum": [
        "info",
        "warning",
        "error"
      ],
      "description": "How much it matters. `warning`: it costs money or loses output. Treat a value you do not know as `info`."
    },
    "code": {
      "type": "string",
      "description": "For programs: stable, one per situation. `unused_translation`: no output carries the translation, which is still produced and billed (and its dub, `params.dubbed`). `unused_dub`: the translation’s subtitles are in an output, its dub in none, and the dub is still produced and billed. `dictionary_not_applied`: a dictionary, postprocessing dictionary or glossary the config names is no longer in the account or in the project’s languages, so the project processes without it (`params.field`, `params.reference`, `params.reason`: `not_found` or `language`). More codes may be added.",
      "examples": [
        "unused_dub",
        "unused_translation",
        "dictionary_not_applied"
      ]
    },
    "path": {
      "type": [
        "string",
        "null"
      ],
      "description": "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.",
      "examples": [
        "/config/translations/0/ttsOptions"
      ]
    },
    "message": {
      "type": "string",
      "description": "For people: what is wrong, what it costs, and what to change."
    },
    "params": {
      "type": "object",
      "description": "The values the message names, for a client that words it itself: `language`, and `dubbed` on `unused_translation`; `field`, `reference` and `reason` on `dictionary_not_applied`.",
      "additionalProperties": {
        "type": [
          "string",
          "number",
          "boolean",
          "null"
        ]
      }
    }
  }
}
```

### Error

`#/components/schemas/Error`

```json
{
  "type": "object",
  "required": [
    "message",
    "code"
  ],
  "properties": {
    "message": {
      "description": "For people. A string, or an array of one string on some statuses: read it as `Array.isArray(m) ? m[0] : m`.",
      "oneOf": [
        {
          "type": "string"
        },
        {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      ]
    },
    "code": {
      "description": "For programs: stable, one per situation. See the Errors guide.",
      "enum": [
        "bad_request",
        "invalid_output_path",
        "invalid_idempotency_key",
        "unauthenticated",
        "invalid_credentials",
        "forbidden",
        "vod_express_not_enabled",
        "account_restricted",
        "not_found",
        "project_not_found",
        "file_not_found",
        "file_meta_not_found",
        "output_not_found",
        "feed_not_found",
        "upload_not_found",
        "webhook_not_found",
        "webhook_delivery_not_found",
        "aws_access_not_provisioned",
        "storage_connection_not_found",
        "method_not_allowed",
        "conflict",
        "file_exists",
        "project_reads_feed",
        "project_reads_folder",
        "file_processing",
        "project_config_locked",
        "idempotency_in_progress",
        "storage_connection_in_use",
        "payload_too_large",
        "unsupported_media_type",
        "validation_failed",
        "service_start_failed",
        "idempotency_key_reused",
        "webhook_limit_reached",
        "storage_failed",
        "storage_connection_check_failed",
        "storage_connection_folder_only",
        "storage_prefix_taken",
        "build_without_storage_connections",
        "build_has_no_schema",
        "too_many_requests",
        "internal_error",
        "bad_gateway",
        "service_stop_failed",
        "aws_access_failed",
        "platform_unavailable",
        "service_unavailable",
        "build_list_unavailable"
      ]
    },
    "errors": {
      "type": "object",
      "description": "On a 422: each field’s messages, keyed by field name, or by JSON pointer inside `config`.",
      "additionalProperties": {
        "type": "array",
        "items": {
          "type": "string"
        }
      }
    }
  }
}
```

### Config

A project’s `config`: every field is in the [Config reference](/vod-express/configuration/reference.md), and `GET /schema` answers it as JSON Schema.
