# Update a project (PUT)

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

Update a project (PUT)

The same as `PATCH`: every field is optional, and a field sent replaces the stored one. A `422` with `service_start_failed` means the change was stored and the restarted service did not start.

**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
{
  "description": "Season 1, dubbed into 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.
