> For a complete page index of the Captain API documentation, fetch https://docs.filesearch.dev/llms.txt?excludeSpec=true

# Index YouTube

POST https://api.filesearch.dev/v2/collections/{collection_name}/index/youtube
Content-Type: application/json

Index YouTube videos into a collection.

Three modes control how each video is processed and billed:

| mode                   | Behavior                                                                                                                                           | Billing                                                      |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `transcript` (default) | Fetches the caption track (auto-generated or manual), formats it with inline timestamps, and indexes it as text. No frames or audio are processed. | Basic text pages (roughly 3 credits for an hour of captions) |
| `audio`                | Pulls the audio track through the multimodal audio pipeline (transcription plus audio understanding).                                              | 5 credits per audio minute                                   |
| `video`                | Pulls the video through the multimodal video pipeline (frame descriptions plus transcription).                                                     | 20 credits per video minute                                  |

The default is `transcript` and will stay `transcript`: media modes cost orders of magnitude more for the same video and are strictly opt-in.

A video with no captions fails in transcript mode by default. Set `on_missing_transcript` to `audio` or `video` to fall back to a media mode for that video instead. Because the fallback changes what you are billed, it is recorded per video: `GET /v2/jobs/{job_id}` returns a `youtube` array with `mode_used`, `fell_back_from`, `transcript_available`, and `languages` for every video in the job.

Accepts either a single `url` string or a `urls` array (max 20; duplicate videos are removed). Supported URL shapes: `youtube.com/watch?v=`, `youtu.be/`, `youtube.com/shorts/`, `youtube.com/embed/`, `youtube.com/v/`, and `youtube.com/live/`. Redirects and shortener URLs are not followed. Live streams are not indexable, and media longer than 4 hours is rejected.

Headers:

* Authorization: Bearer \{api\_key} - Captain API key for authentication
* X-Organization-ID: Organization UUID
* Idempotency-Key: UUID for request deduplication (optional)

Returns:
\{ job\_id, status: "pending" }

Reference: https://docs.filesearch.dev/reference/indexing/youtube/create

## Request

### Path parameters

- `collection_name` (string, required)

### Headers

- `authorization` (string, optional, nullable)

### Body (application/json)

This endpoint expects an IndexYoutubeRequest.

- `custom_metadata` (map from string to IndexYoutubeRequestCustomMetadata, optional, nullable) — Custom metadata to attach to all indexed chunks. Keys must be strings. Values: str, int, float, bool, or List[str].
- `languages` (list of string, optional, nullable) — Preferred transcript languages in priority order (ISO 639-1 codes). Defaults to English. Only specify if you need a non-English transcript (e.g., ['fr', 'de']). Falls back to auto-generated captions if manual transcript unavailable.
- `url` (string, optional, nullable) — A single YouTube video URL (youtube.com/watch?v=, youtu.be/, youtube.com/shorts/). Provide either 'url' or 'urls', not both.
- `urls` (list of string, optional, nullable) — A list of YouTube video URLs to index. Provide either 'url' or 'urls', not both.
- `mask_pii` (boolean, optional, default: false) — When true, detected PII (emails, phone numbers, SSNs, credit cards, names, and locations) is masked in the parsed content before it is embedded and stored — replaced with entity tags like \<PERSON> and \<EMAIL\_ADDRESS>. For images (including images embedded in PDFs), PII text visible in the image is also pixel-redacted. Opt-in; defaults to false, which leaves content unchanged.
- `pii_engine` (enum, optional, default: kev) — Masking engine for this job. `kev` (default): the Kev classifier model on Captain's infrastructure; supports `pii_fields` and `pii_instructions`; no fallback, so a file kev cannot mask fails to index and the rest of the job continues. `jev`: TypeSafe's hosted Jev model, slightly more accurate, best-effort availability; supports custom fields and instructions and may list fallbacks in `pii_fallback`. `presidio`: the legacy pattern engine, built-in categories only (rejects `pii_fields` or `pii_instructions` with 422), never a fallback. Requires `mask_pii: true`. The earlier names `captain-jev` and `captain-presidio` keep working as before; new requests should use `kev`, `jev` or `presidio`.
  - Allowed values: `kev`, `jev`, `presidio`, `captain-jev`, `captain-presidio`
- `pii_fallback` (IndexYoutubeRequestPiiFallback, optional) — Ordered fallback engines for `jev`, as `{"engines": [...], "retry_budget_seconds": n}`. Omitted or an empty `engines` list means no fallback. Only valid with `pii_engine: "jev"`; with `kev` or `presidio` a non-empty list answers 422. On the multipart file upload endpoint, send it as a JSON string form field. With the earlier engine name `captain-jev` it is `true` (default, fall back to `captain-presidio`) or `false`.
- `pii_fields` (list of PIIField, optional, nullable) — Additional PII categories to mask for this job, on top of the built-in set. Requires mask_pii to be true. Up to 20 fields. Each masked value is replaced with the field's tag in angle brackets and appears in the masking report under the field's name.
- `pii_instructions` (string, optional, nullable) — Plain-language guidance that steers what counts as personal data for this job, for the built-in categories and any pii_fields. For example: 'Names of hospital staff may stay; patient names, record numbers and bed assignments must be masked.' Requires mask_pii to be true. Up to 1,000 characters.
- `mode` (enum, optional, default: transcript) — How to index each video. 'transcript' (default): fetch the caption track and index it as text, billed as basic text pages. 'audio': pull the audio track through the multimodal audio pipeline, billed at 5 credits per audio minute. 'video': pull the video (frame descriptions plus transcription) through the multimodal video pipeline, billed at 20 credits per video minute. Media modes cost orders of magnitude more than transcript mode for the same video; the default is transcript and will not change.
  - Allowed values: `transcript`, `audio`, `video`
- `on_missing_transcript` (enum, optional, default: fail) — Only applies when mode='transcript'. What to do for a video whose captions are disabled or absent. 'fail' (default): that video fails. 'audio' / 'video': fall back to indexing that video through the corresponding media pipeline instead, billed at media rates. Any fallback is recorded per video in the job's youtube record (mode_used, fell_back_from) so the billing change is visible. Setting this together with a media mode is rejected with a validation error.
  - Allowed values: `fail`, `audio`, `video`

## Response

### 200

Successful Response

- `job_id` (string, required)
- `status` (string, optional, default: pending)
- `custom_metadata` (map from string to any, optional, nullable) — The custom_metadata Captain accepted for this job, echoed back as validated. Null when none was supplied.

## Types

### IndexYoutubeRequestCustomMetadata

- string
- integer
- double
- boolean
- list of string

### IndexYoutubeRequestPiiFallback

Ordered fallback engines for `jev`, as `{"engines": [...], "retry_budget_seconds": n}`. Omitted or an empty `engines` list means no fallback. Only valid with `pii_engine: "jev"`; with `kev` or `presidio` a non-empty list answers 422. On the multipart file upload endpoint, send it as a JSON string form field. With the earlier engine name `captain-jev` it is `true` (default, fall back to `captain-presidio`) or `false`.

- PiiFallbackPolicy: PiiFallbackPolicy
- boolean

### PIIField

A customer-defined PII category to mask in addition to the built-in ones.

- `name` (string, required) — Tag for this category. Uppercase letters, digits and underscores, 2 to 40 characters, unique within the request and not one of the built-in tags. Masked values are replaced with the tag in angle brackets (a field named EMPLOYEE\_ID produces \<EMPLOYEE\_ID>) and the masking report lists them under this name.
- `description` (string, required) — What the category covers, in plain language, as you would explain it to a colleague. This is what decides whether a value is masked, so say what the values look like and where they appear.
- `examples` (list of string, optional, nullable) — Up to 10 sample values, used only to make the description concrete. They are not a pattern: values that do not look like the examples are still masked when the description covers them.

### PiiFallbackPolicy

Fallback policy for `pii_engine: "jev"`. If every engine fails, the file fails to index and the rest of the job continues.

- `engines` (list of enum, optional, default: []) — Engines to try, in order, when `jev` fails with a rate limit, timeout, capacity limit, provider error or invalid answer. `jev-openrouter` and `jev-ai-gateway` are the same Jev model served through OpenRouter and Vercel AI Gateway. Custom fields and instructions apply on every engine. `presidio` is not allowed, and an engine may appear only once.
  - Allowed values: `jev-openrouter`, `jev-ai-gateway`, `kev`
- `retry_budget_seconds` (integer, optional, default: 60) — Extra retry and wait time per engine, for each piece of a file, before moving to the next engine. Time spent on successful answers does not count. 0 means one attempt per engine. Normal request timeouts still apply.

## Examples

### Index captions (default)

**Request**

```json
{
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
```

**Response**

```json
{
  "job_id": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7-1783111726",
  "status": "pending"
}
```

**SDK Code**

```python Python REST
import requests

BASE_URL = "https://api.filesearch.dev"
API_KEY = "your_api_key"

headers = {"Authorization": f"Bearer {API_KEY}"}

response = requests.post(
    f"{BASE_URL}/v2/collections/my_documents/index/youtube",
    headers=headers,
    json={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
    timeout=60.0,
)
print(response.json())
```

```typescript TypeScript REST
const BASE_URL = "https://api.filesearch.dev";
const API_KEY = "your_api_key";

const response = await fetch(`${BASE_URL}/v2/collections/my_documents/index/youtube`, {
    method: "POST",
    headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"})
});
console.log(await response.json());
```

### Index the full video (20 credits/min)

**Request**

```json
{
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "mode": "video"
}
```

**Response**

```json
{
  "job_id": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7-1783111726",
  "status": "pending"
}
```

**SDK Code**

```python Python REST
import requests

BASE_URL = "https://api.filesearch.dev"
API_KEY = "your_api_key"

headers = {"Authorization": f"Bearer {API_KEY}"}

response = requests.post(
    f"{BASE_URL}/v2/collections/my_documents/index/youtube",
    headers=headers,
    json={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "mode": "video"},
    timeout=60.0,
)
print(response.json())
```

```typescript TypeScript REST
const BASE_URL = "https://api.filesearch.dev";
const API_KEY = "your_api_key";

const response = await fetch(`${BASE_URL}/v2/collections/my_documents/index/youtube`, {
    method: "POST",
    headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "mode": "video"})
});
console.log(await response.json());
```

### Fall back to audio when captions are missing

**Request**

```json
{
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "mode": "transcript",
  "on_missing_transcript": "audio"
}
```

**Response**

```json
{
  "job_id": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7-1783111726",
  "status": "pending"
}
```

**SDK Code**

```python Python REST
import requests

BASE_URL = "https://api.filesearch.dev"
API_KEY = "your_api_key"

headers = {"Authorization": f"Bearer {API_KEY}"}

response = requests.post(
    f"{BASE_URL}/v2/collections/my_documents/index/youtube",
    headers=headers,
    json={
        "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        "mode": "transcript",
        "on_missing_transcript": "audio",
    },
    timeout=60.0,
)
print(response.json())
```

```typescript TypeScript REST
const BASE_URL = "https://api.filesearch.dev";
const API_KEY = "your_api_key";

const response = await fetch(`${BASE_URL}/v2/collections/my_documents/index/youtube`, {
    method: "POST",
    headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
        "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        "mode": "transcript",
        "on_missing_transcript": "audio"
    })
});
console.log(await response.json());
```