> ## Documentation Index
> Fetch the complete documentation index at: https://docs.genviral.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Add Editor Captions

> Transcribe the cut and add timed captions to a video project.

Transcribes the speech in the project's current cut and adds timed caption text clips in the
default caption style, then returns the updated project. Captions follow the cut: each one is
placed on the timeline where its words are spoken, so cut first and caption last.

Adding captions is free (no credits). It can take a while for long videos, and it is rate
limited per account owner because every request transcribes the cut.

The captions appear in the project's `text_clips` with `role: "caption"`, and live in the Genviral
web editor at the project's `editor_url`, where the user can restyle or edit them.

* A cut that already has captions is left as it is, and the project is returned unchanged. Your
  request never adds a second set of captions.
* A cut with no speech is returned unchanged, with no captions added.
* To read the words with their timings instead, use
  [Get Editor Transcript](/api-reference/get-editor-transcript).

## Concurrency

Pass `expected_updated_at` with the `updated_at` you last read. If the project changed since, the
request is refused with `409 editor_conflict`: read the project again and retry with a new
`Idempotency-Key`.

The caption write also checks the revision that was transcribed, even if you omit
`expected_updated_at`. An edit made during transcription returns `409 editor_conflict`
without inserting captions timed to the old cut.

While Vira (the Genviral assistant) is editing the same project, the request is refused with
`409 editor_locked`. Wait a few seconds and retry.

## Idempotency

`Idempotency-Key` is required and is scoped to the API key (or MCP grant) that sends it.

* Retrying with the same key and the same body returns the project and never transcribes or adds
  captions twice.
* Reusing a key with a different body returns `409 idempotency_key_reused`.
* A refused request (for example `502 transcription_failed`) does not consume its key.

## Path Parameters

<ParamField path="projectId" type="string (UUID)" required>
  The project id.
</ParamField>

## Body Parameters

The body is optional. Send `{}` or no body to caption the cut as it is.

<ParamField body="expected_updated_at" type="string">
  The `updated_at` you last read (ISO 8601). The request is refused with `editor_conflict` if the
  project changed since.
</ParamField>

<ParamField body="note" type="string">
  One short plain-text line (at most 140 characters, no line breaks) describing this step in the
  user's terms, for example `Added captions`. The open Genviral web editor shows it live in its
  activity feed, labelled with who made the edit (Claude, ChatGPT, another MCP client, or the API).
</ParamField>

## Response

Returns `200` with the updated project, in the same shape as
[Get Editor Project](/api-reference/get-editor-project). Any earlier render is no longer current
once captions are added.

<RequestExample>
  ```bash cURL theme={null}
  curl --request POST \
    --url https://www.genviral.io/api/partner/v1/editor/projects/b1d4e8f2-3a5c-4d7e-8f90-1a2b3c4d5e6f/captions \
    --header 'Authorization: Bearer <token>' \
    --header 'Content-Type: application/json' \
    --header 'Idempotency-Key: 5a1c7e93-2d4b-4f86-9b0e-3c6d8a2f4e17' \
    --data '{
      "expected_updated_at": "2026-10-02T12:05:00.000Z",
      "note": "Added captions"
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json Response theme={null}
  {
    "ok": true,
    "code": 200,
    "message": "Editor captions added",
    "data": {
      "project_id": "b1d4e8f2-3a5c-4d7e-8f90-1a2b3c4d5e6f",
      "title": "Launch cut",
      "aspect_ratio": "9:16",
      "duration_ms": 37100,
      "updated_at": "2026-10-02T12:06:00.000Z",
      "assets": [],
      "clips": [],
      "text_clips": [
        {
          "clip_id": "caption-8f1d2c3b-4a5e-4f60-9b7c-1d2e3f4a5b6c",
          "text": "this changed how I plan",
          "start_ms": 0,
          "end_ms": 1450,
          "role": "caption"
        },
        {
          "clip_id": "caption-2b3c4d5e-6f70-4a81-9c2d-3e4f5a6b7c8d",
          "text": "my whole week",
          "start_ms": 1450,
          "end_ms": 2300,
          "role": "caption"
        }
      ],
      "undo": null,
      "render": null,
      "editor_url": "https://www.genviral.io/editor/b1d4e8f2-3a5c-4d7e-8f90-1a2b3c4d5e6f?workspace=personal"
    }
  }
  ```
</ResponseExample>

## Error Responses

* `400 invalid_json` - request body is not valid JSON
* `400 invalid_request` - `Idempotency-Key` header is missing or longer than 255 characters
* `401` - authentication failed (missing/invalid/revoked token)
* `404 project_not_found` - the project does not exist in the key scope
* `409 editor_locked` - Vira is editing this project right now; retry shortly
* `409 editor_conflict` - the project changed since your read or during transcription; read it again
* `409 idempotency_key_reused` - this `Idempotency-Key` was already used with a different body
* `409 request_in_progress` - an identical request with this key is still being processed; retry shortly
* `422 invalid_payload` - body failed validation (see `issues`)
* `422 no_audio_source` - nothing on the timeline has audible sound to caption
* `422 invalid_document` - the project could not be read or the result would not be a valid project
* `429 rate_limited` - the hourly caption budget is spent; retry after `retry_after_seconds`
* `502 transcription_failed` - the cut could not be transcribed; retry later


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.