# How does the TubeTranscript REST API v1 work?

The TubeTranscript REST API v1 exposes one synchronous endpoint, **`POST /api/v1/extract`**, that returns a YouTube video's **timestamped transcript** and optional AI features in a single **JSON payload**. It requires a **Pro plan API key** sent as a **Bearer Token**, and every response uses one envelope: `success`, `data`, `error`.

For a shorter introduction with curl, Python and JavaScript samples, read the [YouTube transcript API overview](https://tubetranscript.io/youtube-transcript-api).

## How do I call the extract endpoint?

Send **`POST https://tubetranscript.io/api/v1/extract`** with header **`Content-Type: application/json`** and a JSON body containing a YouTube **`url`**. The optional **`features`** array requests AI outputs. `GET`, `PUT`, `PATCH` and `DELETE` return **`405 METHOD_NOT_ALLOWED`**. Set your HTTP client timeout to **300 seconds**, because audio transcription can take minutes.

```bash
curl -X POST https://tubetranscript.io/api/v1/extract \
  -H "Authorization: Bearer $TUBETRANSCRIPT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw", "features": ["hooks", "mindmap", "action_items"]}'
```

| Property       | Value                                                               |
| -------------- | ------------------------------------------------------------------- |
| Method         | `POST`                                                              |
| URL            | `https://tubetranscript.io/api/v1/extract`                          |
| Headers        | `Authorization: Bearer <API_KEY>`, `Content-Type: application/json` |
| Client timeout | 300 s                                                               |
| Other methods  | `GET`, `PUT`, `PATCH`, `DELETE` → `405 METHOD_NOT_ALLOWED`          |

| Field      | Type     | Required | Default                | Constraints                                                                                  |
| ---------- | -------- | -------- | ---------------------- | -------------------------------------------------------------------------------------------- |
| `url`      | string   | yes      | none                   | Any YouTube video URL: `watch`, `youtu.be`, `shorts`, `embed`, `live`. Max 2,048 characters. |
| `features` | string[] | no       | `[]` (transcript only) | Any of `hooks`, `mindmap`, `action_items`, each at most once.                                |

---

## How do I authenticate API requests?

Authenticate every request with the header **`Authorization: Bearer <API_KEY>`**. Only API keys belonging to a team on the **Pro plan** are accepted. A missing or unknown key returns **`401`** with `MISSING_API_KEY` or `INVALID_API_KEY`; a key whose team is not Pro returns **`403 PLAN_UPGRADE_REQUIRED`**. Key handling rules are in [Security & Rate Limits](https://tubetranscript.io/docs/security.md).

```http
Authorization: Bearer <API_KEY>
```

| Condition                         | HTTP | `error.code`            | Extra header               |
| --------------------------------- | ---- | ----------------------- | -------------------------- |
| No `Authorization` header         | 401  | `MISSING_API_KEY`       | `WWW-Authenticate: Bearer` |
| Malformed, unknown or revoked key | 401  | `INVALID_API_KEY`       | `WWW-Authenticate: Bearer` |
| Key's team not on Pro             | 403  | `PLAN_UPGRADE_REQUIRED` | none                       |

---

## What JSON does the endpoint return?

Every response, success or error, returns the same envelope: **`success`** (boolean), **`data`** (result object or `null`), and **`error`** (`null` or an object with `code`, `message`, optional `details`). On success, `data` holds **`video_id`**, **`source`**, **`language`**, the **`transcript`** as plain text plus timed **`chunks`**, and requested **`features`**. Responses carry **`Cache-Control: no-store`**.

```json
{
  "success": true,
  "data": {
    "video_id": "jNQXAC9IVRw",
    "job_id": null,
    "source": "captions",
    "language": "en",
    "video_title": null,
    "channel_name": null,
    "transcript": {
      "text": "…",
      "chunks": [{ "start": 0, "duration": 4.2, "text": "…" }]
    },
    "features": {
      "hooks": [{ "start_time": "…", "end_time": "…", "hook_title": "…", "summary": "…" }],
      "mindmap": "mindmap\n  Root\n    Branch",
      "action_items": [{ "assignee": null, "task": "…", "deadline_mentioned": null }]
    }
  },
  "error": null
}
```

```json
{ "success": false, "data": null, "error": { "code": "INVALID_INPUT", "message": "…" } }
```

| Envelope field | Type           | On success    | On error                           |
| -------------- | -------------- | ------------- | ---------------------------------- |
| `success`      | boolean        | `true`        | `false`                            |
| `data`         | object \| null | Result object | `null`, except for `202` and `502` |
| `error`        | object \| null | `null`        | `{ code, message, details? }`      |

| `data` field        | Type           | Notes                                                           |
| ------------------- | -------------- | --------------------------------------------------------------- |
| `video_id`          | string         | 11-character YouTube video ID parsed from `url`.                |
| `job_id`            | string \| null | Set when the transcript came from an extraction job.            |
| `source`            | string         | `captions`, `audio` or `existing_job`.                          |
| `language`          | string \| null | Caption language code; set when `source` is `captions`.         |
| `video_title`       | string \| null | Set when the transcript came from a job.                        |
| `channel_name`      | string \| null | Set when the transcript came from a job.                        |
| `transcript.text`   | string         | All chunk texts joined by blank lines (non-timestamped).        |
| `transcript.chunks` | array          | `{ start, duration, text }`, `start` and `duration` in seconds. |
| `features`          | object         | Only the features requested in the body.                        |

### What does the `source` field mean?

The **`source`** field reports where the transcript came from. **`captions`** means the video's YouTube captions were fetched synchronously (Tier 1). **`audio`** means the audio was transcribed with **Whisper** (Tier 2). **`existing_job`** means your team already had a completed or running job for the same video, so no new extraction ran.

| `source` value | Engine                         | Typical latency                       | `job_id` |
| -------------- | ------------------------------ | ------------------------------------- | -------- |
| `existing_job` | Reused team job                | Instant, or waits for the running job | Set      |
| `captions`     | YouTube captions (Tier 1)      | Seconds                               | `null`   |
| `audio`        | Whisper transcription (Tier 2) | Minutes                               | Set      |

---

## How does TubeTranscript choose between captions and Whisper?

TubeTranscript picks the cheapest available transcript source automatically, in a fixed order. It first reuses your team's existing job for the video, then fetches **YouTube captions (Tier 1, seconds)**, and falls back to **Whisper audio transcription (Tier 2, minutes)** only when the video has no captions. Callers never select the engine; the same request works for every video.

1. **Existing job**: a completed or still-running job of your team for the same video (the call waits for a running one).
2. **Tier 1, captions**: returned in seconds when the video has captions.
3. **Tier 2, Whisper**: audio transcription when no captions exist; can take minutes.

### What happens when Whisper transcription takes too long?

A Tier 2 call waits up to about **280 seconds** without features, or about **210 seconds** with features, staying inside the **300-second** timeout of Make and n8n. If Whisper is still running, the API returns **`202 EXTRACTION_IN_PROGRESS`** with `data.job_id` and **`Retry-After: 60`**. Send the identical request again; it resumes the same job.

| Request            | Maximum wait for Whisper | Response if unfinished                          |
| ------------------ | ------------------------ | ----------------------------------------------- |
| Without `features` | ~280 s                   | `202 EXTRACTION_IN_PROGRESS`, `Retry-After: 60` |
| With `features`    | ~210 s                   | `202 EXTRACTION_IN_PROGRESS`, `Retry-After: 60` |

---

## What are the API rate limits?

Each **Pro API key** allows **1000 requests per 24 hours**, and every call to `/api/v1/extract` counts once, regardless of how many features it requests. Remaining quota is reported in the **`X-RateLimit-Limit`**, **`X-RateLimit-Remaining`** and **`X-RateLimit-Reset`** response headers. Exceeding it returns **`429 RATE_LIMIT_EXCEEDED`** with a **`Retry-After`** header. Details: [Security & Rate Limits](https://tubetranscript.io/docs/security.md).

| Tier               | Request Limit                     | REST API v1 | MCP Support                        | Transcript Storage                           |
| ------------------ | --------------------------------- | ----------- | ---------------------------------- | -------------------------------------------- |
| Guest (no account) | 10 transcripts / 24 h per IP (max 5 Whisper) | No          | No                                 | Public page under `/c/`, no automatic expiry |
| Free account       | 24 transcripts / 24 h per team    | No          | No                                 | Public page under `/c/`, no automatic expiry |
| Pro                | 1,000 requests / 24 h per API key | Yes         | Yes (`/api/mcp`, `/api/mcp/tools`) | Private to the team, no automatic expiry     |

| Response header         | Meaning                                      |
| ----------------------- | -------------------------------------------- |
| `X-RateLimit-Limit`     | Requests allowed per 24-hour sliding window. |
| `X-RateLimit-Remaining` | Requests left in the current window.         |
| `X-RateLimit-Reset`     | When the window resets.                      |
| `Retry-After`           | Seconds to wait; sent with `429`.            |

---

## Which error codes can the API return?

Errors use the standard envelope with **`success: false`** and a machine-readable **`error.code`**. Branch on `error.code`, not on the message text. Only **`202 EXTRACTION_IN_PROGRESS`** and **`502 FEATURES_FAILED`** include a non-null `data`: the first carries the job ID to retry, the second carries the transcript and every feature that succeeded.

| HTTP | `error.code`             | Meaning                                                                                                       | Action                                               | Retry |
| ---- | ------------------------ | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ----- |
| 202  | `EXTRACTION_IN_PROGRESS` | Whisper still running.                                                                                        | Resend the same request after `Retry-After` seconds. | Yes   |
| 400  | `INVALID_JSON`           | Body is not valid JSON.                                                                                       | Fix the request body.                                | No    |
| 400  | `INVALID_INPUT`          | `url` or `features` is invalid.                                                                               | Fix the field named in `message`.                    | No    |
| 401  | `MISSING_API_KEY`        | No `Authorization` header.                                                                                    | Send `Authorization: Bearer <API_KEY>`.              | No    |
| 401  | `INVALID_API_KEY`        | Malformed, unknown or revoked key.                                                                            | Use a valid Pro key.                                 | No    |
| 403  | `PLAN_UPGRADE_REQUIRED`  | Key's team is not on Pro.                                                                                     | Upgrade the team to Pro.                             | No    |
| 404  | `TRANSCRIPT_NOT_FOUND`   | Video unavailable.                                                                                            | Check the URL.                                       | No    |
| 405  | `METHOD_NOT_ALLOWED`     | Method other than `POST`.                                                                                     | Use `POST`.                                          | No    |
| 422  | `EXTRACTION_FAILED`      | Video could not be transcribed (private, removed, no speech).                                                 | Try another video.                                   | No    |
| 429  | `RATE_LIMIT_EXCEEDED`    | 24-hour quota used up.                                                                                        | Wait `Retry-After` seconds.                          | Yes   |
| 500  | `INTERNAL_ERROR`         | Unexpected server error.                                                                                      | Retry later.                                         | Yes   |
| 500  | `WORKER_ERROR`           | Extraction worker error.                                                                                      | Retry later.                                         | Yes   |
| 502  | `FEATURES_FAILED`        | Some AI features failed; `data` holds the transcript and successful features, `error.details` lists failures. | Retry for the failed features.                       | Yes   |
| 503  | `WORKER_UNAVAILABLE`     | Extraction service down or in maintenance.                                                                    | Retry later.                                         | Yes   |
| 503  | `RATE_LIMIT_UNAVAILABLE` | Quota store unreachable.                                                                                      | Wait `Retry-After` seconds (60).                     | Yes   |

---

## Which AI features can I request?

The **`features`** array accepts three values: **`hooks`** (timestamped attention hooks with title and summary), **`mindmap`** (a **Mermaid mindmap** string), and **`action_items`** (tasks with assignee and mentioned deadline). Transcripts above **120,000 characters**, roughly **2 hours** of speech, are processed in segments and merged. An empty array returns the transcript only.

| `features` value | Output in `data.features` | Item fields                                       |
| ---------------- | ------------------------- | ------------------------------------------------- |
| `hooks`          | Array of hooks            | `start_time`, `end_time`, `hook_title`, `summary` |
| `mindmap`        | Mermaid mindmap string    | none                                              |
| `action_items`   | Array of tasks            | `assignee`, `task`, `deadline_mentioned`          |

---

## How do I call the creator tools?

**`POST /api/v1/creator/{tool}`** runs one creator tool on a video and answers in the same envelope as `/api/v1/extract`. It needs a **Pro API key**. Send exactly one of `job_id` (a completed job of your team, from `/api/v1/extract` or the MCP `get_transcript`) or `url` (a YouTube link; captions only, nothing is stored), plus optional `options`.

```bash
curl -X POST https://tubetranscript.io/api/v1/creator/generate_chapters \
  -H "Authorization: Bearer $TUBETRANSCRIPT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "options": {"count": 8}}'
```

| `{tool}`               | `options`                                                                           | `data` (besides `tool`, `video_id`)                         |
| ---------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `generate_chapters`    | `style` (`short`, `descriptive`, `emoji`), `count` (3-40)                          | `chapters` (`start`, `clock`, `title`), `text`               |
| `generate_description` | `tone` (`neutral`, `friendly`, `professional`, `energetic`), `cta`, `chapters`     | `description`, `tags`, `hashtags`, `chars`                   |
| `find_moments`         | `length` (`any`, `short`, `medium`, `long`)                                        | `moments` (`start`, `end`, `duration`, `hook`, `reason`, `quote`, `standalone`) |
| `analyze_hook`         | `topic` (for rewritten hooks)                                                       | `opening`, `hookType`, `parts`, `pace`, `rewrites`           |
| `translate_subtitles`  | `target_languages` (1-5 ISO codes), `format` (`srt`, `vtt`, `txt`), `glossary`    | `outputs` (`language`, `name`, `text`, `untranslated`, `warnings`), `failed` |

Times are taken from the transcript lines by code; the model never writes them. Every item is checked against the transcript, and items that fail are left out.

**Limits.** Each call counts once against the Pro daily limit and is one creator-tool AI run (**30 per hour** per team; `translate_subtitles` counts one per language). A run is charged only when it returns a result: `UNRELIABLE` (too few results passed the checks) and provider errors are free, though at most **90 model calls per hour** are allowed. A result already made for the same public video and transcript is returned free. One run per team at a time; a second one gets `BUSY`.

| HTTP | `error.code`                                                        |
| ---- | ------------------------------------------------------------------- |
| 400  | `INVALID_JSON`, `INVALID_INPUT`                                     |
| 404  | `UNKNOWN_TOOL`, `JOB_NOT_FOUND`, `TRANSCRIPT_NOT_FOUND`             |
| 409  | `JOB_NOT_READY`, `BUSY` (another run of your team is working)      |
| 413  | `VIDEO_TOO_LONG` (over 120 minutes), `TRANSCRIPT_TOO_LONG`, `FILE_TOO_LARGE` |
| 422  | `UNRELIABLE`                                                        |
| 429  | `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `BUDGET_REACHED`           |
| 502  | `AI_*` provider or validation errors                                |
| 503  | `AI_DISABLED`, `QUOTA_UNAVAILABLE`, `WORKER_UNAVAILABLE`            |
