---
title: Add captions
description: Add captions and subtitles to Cloudflare Stream videos using AI generation or file upload.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/stream/llms.txt  
> Use this file to discover all available pages before exploring further.

[Skip to content](#%5Ftop) 

# Add captions

Adding captions and subtitles to your video library.

## Add or modify a caption

There are two ways to add captions to a video: generating via AI or uploading a caption file.

To create or modify a caption on a video a [Cloudflare API Token ↗](https://www.cloudflare.com/a/account/my-account) is required.

The `<LANGUAGE_TAG>` must adhere to the [BCP 47 format ↗](http://www.unicode.org/reports/tr35/#Unicode%5FLanguage%5Fand%5FLocale%5FIdentifiers). For convenience, many common language codes are provided [at the bottom of this document](#most-common-language-codes). If the language you are adding is not included in the table, you can find the value through the [The IANA registry ↗](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry), which maintains a list of language codes. To find the value to send, search for the language. Below is an example value from IANA when we look for the value to send for a Turkish subtitle:

Terminal window

```

%%


Subtag: tr

Description: Turkish

Added: 2005-10-16

Suppress-Script: Latn

%%


```

The `Subtag` code indicates a value of `tr`. This is the value you should send as the `language` at the end of the HTTP request.

A label is generated from the provided language. The label will be visible for user selection in the player. For example, if sent `tr`, the label `Türkçe` will be created; if sent `de`, the label `Deutsch` will be created.

### Generate a caption

Generated captions use artificial intelligence based speech-to-text technology to generate closed captions for your videos.

A video must be uploaded and in a ready state before captions can be generated. In the following example URLs, the video's UID is referenced as `<VIDEO_UID>`. To receive webhooks when a video transitions to ready after upload, follow the instructions provided in [using webhooks](https://developers.cloudflare.com/stream/manage-video-library/using-webhooks/).

Captions can be generated for the following languages:

* `cs` \- Czech
* `nl` \- Dutch
* `en` \- English
* `fr` \- French
* `de` \- German
* `it` \- Italian
* `ja` \- Japanese
* `ko` \- Korean
* `pl` \- Polish
* `pt` \- Portuguese
* `ru` \- Russian
* `es` \- Spanish

When generating captions, generate them for the spoken language in the audio.

Videos may include captions for several languages, but each language must be unique. For example, a video may have English, French, and German captions associated with it, but it cannot have two English captions. If you have already uploaded an English language caption for a video, you must first delete it in order to create an English generated caption. Instructions on how to delete a caption can be found below.

The `<LANGUAGE_TAG>` must adhere to the BCP 47 format. The tag for English is `en`. You may specify a region in the tag, such as `en-GB`, which will render a label that shows `British English` for the caption.

* [ REST API ](#tab-panel-8320)
* [ Workers Binding API ](#tab-panel-8321)

* [ cURL ](#tab-panel-8304)
* [ TypeScript ](#tab-panel-8305)

Terminal window

```

curl -X POST \

-H 'Authorization: Bearer <API_TOKEN>' \

https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/stream/<VIDEO_UID>/captions/<LANGUAGE_TAG>/generate


```

TypeScript

```

const client = new Cloudflare({

  apiEmail: process.env['CLOUDFLARE_EMAIL'],

  apiKey: process.env['CLOUDFLARE_API_KEY'],

});


const caption = await client.stream.captions.language.create("<VIDEO_UID>", "en", {

  account_id: '<ACCOUNT_ID>',

});


```

See the full Stream [REST API and SDK reference](https://developers.cloudflare.com/api/resources/stream/) for details on using REST API from external applications, with pre-generated SDK's for external TypeScript, Python, or Go applications.

* [ index.ts ](#tab-panel-8306)
* [ wrangler.jsonc ](#tab-panel-8307)

TypeScript

```

export default {

  async fetch(request, env, ctx): Promise<Response> {

    const videoId = "<VIDEO_UID>";

    const caption = await env.STREAM.video(videoId).captions.generate("en");

    return new Response(JSON.stringify({ caption }));

  },

} satisfies ExportedHandler<{ STREAM: StreamBinding }>;


```

```

{

  "$schema": "node_modules/wrangler/config-schema.json",

  "name": "<ENTER_WORKER_NAME>",

  "main": "src/index.ts",

  "compatibility_date": "$today",

  "observability": {

    "enabled": true

  },

  "stream": {

    "binding": "STREAM"

  }

}


```

See the full [Workers Stream binding API reference](https://developers.cloudflare.com/stream/manage-video-library/bindings/).

Example response:

```

{

  "result": {

    "language": "en",

    "label": "English (auto-generated)",

    "generated": true,

    "status": "inprogress"

  },

  "success": true,

  "errors": [],

  "messages": []

}


```

The result will provide a `status` denoting the progress of the caption generation.  
There are three statuses: inprogress, ready, and error. Note that (auto-generated) is applied to the label.

Once the generated caption is ready, it will automatically appear in the video player and video manifest.

If the caption enters an error state, you may attempt to re-generate it by first deleting it and then using the endpoint listed above. Instructions on deletion are provided below.

### Upload a file

Note two changes if you edit a generated caption: the generated field will change to `false` and the (auto-generated) portion of the label will be removed.

To create or replace a caption file:

* [ REST API ](#tab-panel-8322)
* [ Workers Binding API ](#tab-panel-8323)

* [ cURL ](#tab-panel-8308)
* [ TypeScript ](#tab-panel-8309)

Terminal window

```

curl -X PUT \

 -H 'Authorization: Bearer <API_TOKEN>' \

 -F file=@/Users/mickie/Desktop/example_caption.vtt \

https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/stream/<VIDEO_UID>/captions/<LANGUAGE_TAG>


```

TypeScript

```

const client = new Cloudflare({

  apiEmail: process.env['CLOUDFLARE_EMAIL'],

  apiKey: process.env['CLOUDFLARE_API_KEY'],

});


const caption = await client.stream.captions.language.update("<VIDEO_UID>", "en", {

  account_id: '<ACCOUNT_ID>',

  file: '@/path/to/caption.vtt',

});


```

See the full Stream [REST API and SDK reference](https://developers.cloudflare.com/api/resources/stream/) for details on using REST API from external applications, with pre-generated SDK's for external TypeScript, Python, or Go applications.

* [ index.ts ](#tab-panel-8310)
* [ wrangler.jsonc ](#tab-panel-8311)

TypeScript

```

export default {

  async fetch(request, env, ctx): Promise<Response> {

    const videoId = "<VIDEO_UID>";

    const language = "en";

    // Obtain a ReadableStream from a file upload, fetch, or other source

    const captionStream: ReadableStream = request.body!;

    const caption = await env.STREAM.video(videoId).captions.upload(language, captionStream);

    return new Response(JSON.stringify({ caption }));

  },

} satisfies ExportedHandler<{ STREAM: StreamBinding }>;


```

```

{

  "$schema": "node_modules/wrangler/config-schema.json",

  "name": "<ENTER_WORKER_NAME>",

  "main": "src/index.ts",

  "compatibility_date": "$today",

  "observability": {

    "enabled": true

  },

  "stream": {

    "binding": "STREAM"

  }

}


```

See the full [Workers Stream binding API reference](https://developers.cloudflare.com/stream/manage-video-library/bindings/).

### Example Response to Add or Modify a Caption

```

{

  "result": {

    "language": "en",

    "label": "English",

    "generated": false,

    "status": "ready"

  },

  "success": true,

  "errors": [],

  "messages": []

}


```

## List the captions associated with a video

To view captions associated with a video. Note this results list will also include generated captions that are `inprogress`and `error` status:

* [ REST API ](#tab-panel-8324)
* [ Workers Binding API ](#tab-panel-8325)

* [ cURL ](#tab-panel-8312)
* [ TypeScript ](#tab-panel-8313)

Terminal window

```

curl -H 'Authorization: Bearer <API_TOKEN>' \

https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/stream/<VIDEO_UID>/captions


```

TypeScript

```

const client = new Cloudflare({

  apiEmail: process.env['CLOUDFLARE_EMAIL'],

  apiKey: process.env['CLOUDFLARE_API_KEY'],

});


const captions = await client.stream.captions.get("<VIDEO_UID>", {

  account_id: '<ACCOUNT_ID>',

});


```

See the full Stream [REST API and SDK reference](https://developers.cloudflare.com/api/resources/stream/) for details on using REST API from external applications, with pre-generated SDK's for external TypeScript, Python, or Go applications.

* [ index.ts ](#tab-panel-8314)
* [ wrangler.jsonc ](#tab-panel-8315)

TypeScript

```

export default {

  async fetch(request, env, ctx): Promise<Response> {

    const videoId = "<VIDEO_UID>";

    const captions = await env.STREAM.video(videoId).captions.list();

    return new Response(JSON.stringify({ captions }));

  },

} satisfies ExportedHandler<{ STREAM: StreamBinding }>;


```

```

{

  "$schema": "node_modules/wrangler/config-schema.json",

  "name": "<ENTER_WORKER_NAME>",

  "main": "src/index.ts",

  "compatibility_date": "$today",

  "observability": {

    "enabled": true

  },

  "stream": {

    "binding": "STREAM"

  }

}


```

See the full [Workers Stream binding API reference](https://developers.cloudflare.com/stream/manage-video-library/bindings/).

### Example response to get the captions associated with a video

```

{

  "result": [

    {

      "language": "en",

      "label": "English (auto-generated)",

      "generated": true,

      "status": "inprogress"

    },

    {

      "language": "de",

      "label": "Deutsch",

      "generated": false,

      "status": "ready"

    }

  ],

  "success": true,

  "errors": [],

  "messages": []

}


```

## Fetch a caption file

To view the WebVTT caption file, you may make a GET request:

Terminal window

```

curl \

-H 'Authorization: Bearer <API_TOKEN>' \

https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/stream/<VIDEO_UID>/captions/<LANGUAGE_TAG>/vtt


```

### Example response to get the caption file for a video

```

WEBVTT


1

00:00:00.000 --> 00:00:01.560

This is an example of


2

00:00:01.560 --> 00:00:03.880

a WebVTT caption response.


```

## Delete the captions

To remove a caption associated with your video:

* [ REST API ](#tab-panel-8326)
* [ Workers Binding API ](#tab-panel-8327)

* [ cURL ](#tab-panel-8316)
* [ TypeScript ](#tab-panel-8317)

Terminal window

```

curl -X DELETE \

 -H 'Authorization: Bearer <API_TOKEN>' \

 https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/stream/<VIDEO_UID>/captions/<LANGUAGE_TAG>


```

TypeScript

```

const client = new Cloudflare({

  apiEmail: process.env['CLOUDFLARE_EMAIL'],

  apiKey: process.env['CLOUDFLARE_API_KEY'],

});


await client.stream.captions.language.delete("<VIDEO_UID>", "en", {

  account_id: '<ACCOUNT_ID>',

});


```

See the full Stream [REST API and SDK reference](https://developers.cloudflare.com/api/resources/stream/) for details on using REST API from external applications, with pre-generated SDK's for external TypeScript, Python, or Go applications.

* [ index.ts ](#tab-panel-8318)
* [ wrangler.jsonc ](#tab-panel-8319)

TypeScript

```

export default {

  async fetch(request, env, ctx): Promise<Response> {

    const videoId = "<VIDEO_UID>";

    await env.STREAM.video(videoId).captions.delete("en");

    return new Response(JSON.stringify({ success: true }));

  },

} satisfies ExportedHandler<{ STREAM: StreamBinding }>;


```

```

{

  "$schema": "node_modules/wrangler/config-schema.json",

  "name": "<ENTER_WORKER_NAME>",

  "main": "src/index.ts",

  "compatibility_date": "$today",

  "observability": {

    "enabled": true

  },

  "stream": {

    "binding": "STREAM"

  }

}


```

See the full [Workers Stream binding API reference](https://developers.cloudflare.com/stream/manage-video-library/bindings/).

If there is an entry in `errors` response field, the caption has not been deleted.

### Example response to delete the caption

```

{

  "result": "",

  "success": true,

  "errors": [],

  "messages": []

}


```

## Limitations

* A video must be uploaded before a caption can be attached to it. In the following example URLs, the video's ID is referenced as `media_id`.
* Stream only supports [WebVTT ↗](https://developer.mozilla.org/en-US/docs/Web/API/WebVTT%5FAPI)formatted caption files. If you have a differently formatted caption file, use [a tool to convert your file to WebVTT ↗](https://subtitletools.com/convert-to-vtt-online)prior to uploading it.
* Videos may include several language captions, but each language must be unique. For example, a video may have English, French, and German captions associated with it, but it cannot have two French captions.
* Each caption file is limited to 10 MB in size. [Contact support](https://developers.cloudflare.com/support/contacting-cloudflare-support/)if you need to upload a larger file.

## Most common language codes

| Language Code | Language         |
| ------------- | ---------------- |
| zh            | Mandarin Chinese |
| hi            | Hindi            |
| es            | Spanish          |
| en            | English          |
| ar            | Arabic           |
| pt            | Portuguese       |
| bn            | Bengali          |
| ru            | Russian          |
| ja            | Japanese         |
| de            | German           |
| pa            | Panjabi          |
| jv            | Javanese         |
| ko            | Korean           |
| vi            | Vietnamese       |
| fr            | French           |
| ur            | Urdu             |
| it            | Italian          |
| tr            | Turkish          |
| fa            | Persian          |
| pl            | Polish           |
| uk            | Ukrainian        |
| my            | Burmese          |
| th            | Thai             |

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/stream/","name":"Stream"}},{"@type":"ListItem","position":3,"item":{"@id":"/stream/edit-videos/","name":"Edit videos"}},{"@type":"ListItem","position":4,"item":{"@id":"/stream/edit-videos/adding-captions/","name":"Add captions"}}]}
```
