Toucora API
🌐 Language
Examples · v1

Copy-paste code examples

Working Python, JavaScript and cURL snippets for the Toucora API. Base URL: https://api.example.com/v1. Every request uses Authorization: Bearer YOUR_API_KEY; get a key from the dashboard.

Before you copy — set TOUCORA_API_KEY in your environment (or replace YOUR_API_KEY). The host https://api.example.com below is rewritten to this deployment when the page loads. See the docs for the full endpoint reference and the OpenAPI spec.

Get a transcript

GET/v1/transcripts/{videoId} returns a normalized JSON transcript with per-segment timings. Pass the video id or a full YouTube URL.

curl "https://api.example.com/v1/transcripts/dQw4w9WgXcQ?lang=en" \
  -H "Authorization: Bearer YOUR_API_KEY"
import os
import requests

API = "https://api.example.com/v1"
API_KEY = os.environ["TOUCORA_API_KEY"]

resp = requests.get(
    f"{API}/transcripts/dQw4w9WgXcQ",
    params={"lang": "en"},
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=30,
)
resp.raise_for_status()
transcript = resp.json()

print(transcript["title"], "|", transcript["language"], "|", transcript["duration"], "s")
for segment in transcript["segments"]:
    print(f'{segment["start"]:7.2f}  {segment["text"]}')
const API = "https://api.example.com/v1";
const API_KEY = process.env.TOUCORA_API_KEY ?? "YOUR_API_KEY";

const res = await fetch(`${API}/transcripts/dQw4w9WgXcQ?lang=en`, {
  headers: { Authorization: `Bearer ${API_KEY}` },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${res.status} ${error.code}: ${error.message}`);
}

const transcript = await res.json();
console.log(`${transcript.title} | ${transcript.language} | ${transcript.duration}s`);
for (const { start, text } of transcript.segments) console.log(start, text);

Need a specific caption track? Call GET /v1/transcripts/{videoId}/languages first, then pass ?lang=. A batch of up to 50 ids is one POST /v1/transcripts with {"ids": [...]}.

Export formats

Add ?format= to get subtitles instead of JSON. txt (plain text), srt, vtt and markdown stream as text and preserve timing for SRT/VTT. timestamps=true prefixes TXT lines with [mm:ss]; clean=true strips [Music] and similar cues.

# TXT with timestamps, then SRT, VTT and Markdown sub-titles.
curl "https://api.example.com/v1/transcripts/dQw4w9WgXcQ?format=txt&timestamps=true&clean=true" \
  -H "Authorization: Bearer YOUR_API_KEY" -o transcript.txt

curl "https://api.example.com/v1/transcripts/dQw4w9WgXcQ?format=srt" \
  -H "Authorization: Bearer YOUR_API_KEY" -o transcript.srt

curl "https://api.example.com/v1/transcripts/dQw4w9WgXcQ?format=vtt" \
  -H "Authorization: Bearer YOUR_API_KEY" -o transcript.vtt

curl "https://api.example.com/v1/transcripts/dQw4w9WgXcQ?format=markdown" \
  -H "Authorization: Bearer YOUR_API_KEY" -o transcript.md
import os
import requests

API = "https://api.example.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['TOUCORA_API_KEY']}"}
VIDEO = "dQw4w9WgXcQ"

# json (default) returns structured segments; the other formats return text.
for fmt, ext in [("txt", "txt"), ("srt", "srt"), ("vtt", "vtt"), ("markdown", "md")]:
    resp = requests.get(
        f"{API}/transcripts/{VIDEO}",
        params={"lang": "en", "format": fmt, "timestamps": "true", "clean": "true"},
        headers=HEADERS,
        timeout=30,
    )
    resp.raise_for_status()
    with open(f"{VIDEO}.{ext}", "w", encoding="utf-8") as fh:
        fh.write(resp.text)
    print(f"wrote {VIDEO}.{ext} ({len(resp.text)} bytes)")
import { writeFile } from "node:fs/promises";

const API = "https://api.example.com/v1";
const API_KEY = process.env.TOUCORA_API_KEY ?? "YOUR_API_KEY";
const VIDEO = "dQw4w9WgXcQ";

for (const [format, ext] of [["txt", "txt"], ["srt", "srt"], ["vtt", "vtt"], ["markdown", "md"]]) {
  const url = new URL(`${API}/transcripts/${VIDEO}`);
  url.searchParams.set("format", format);
  url.searchParams.set("timestamps", "true");
  url.searchParams.set("clean", "true");

  const res = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } });
  if (!res.ok) throw new Error(`export failed: HTTP ${res.status}`);
  const text = await res.text();
  await writeFile(`${VIDEO}.${ext}`, text);
  console.log(`wrote ${VIDEO}.${ext} (${text.length} bytes)`);
}

Translate

POST/v1/translate translates a transcript while preserving timings. mode is translated (default), bilingual, or original. When no AI provider is configured the original transcript is returned with "fallback": true.

curl -X POST "https://api.example.com/v1/translate" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"video_id":"dQw4w9WgXcQ","target_lang":"es","mode":"bilingual"}'
import os
import requests

API = "https://api.example.com/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['TOUCORA_API_KEY']}",
    "Content-Type": "application/json",
}

resp = requests.post(
    f"{API}/translate",
    headers=HEADERS,
    json={"video_id": "dQw4w9WgXcQ", "target_lang": "es", "mode": "bilingual"},
    timeout=120,
)
resp.raise_for_status()
data = resp.json()

print(data["target_lang"], "translated" if data["translated"] else "fallback")
for segment in data["segments"]:
    print(segment["text"])
const API = "https://api.example.com/v1";
const API_KEY = process.env.TOUCORA_API_KEY ?? "YOUR_API_KEY";

const res = await fetch(`${API}/translate`, {
  method: "POST",
  headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ video_id: "dQw4w9WgXcQ", target_lang: "es", mode: "bilingual" }),
});
if (!res.ok) throw new Error((await res.json()).error.message);

const data = await res.json();
console.log(data.target_lang, data.translated ? "translated" : `fallback: ${data.reason ?? "original"}`);
for (const segment of data.segments) console.log(segment.text);

Summarize

POST/v1/summarize returns a TL;DR, key points and topics. length is short, medium, or detailed; output_lang writes the result in another language. When AI is disabled you still get an extractive summary with "fallback": true.

curl -X POST "https://api.example.com/v1/summarize" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"video_id":"dQw4w9WgXcQ","length":"short","output_lang":"en"}'
import os
import requests

API = "https://api.example.com/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['TOUCORA_API_KEY']}",
    "Content-Type": "application/json",
}

resp = requests.post(
    f"{API}/summarize",
    headers=HEADERS,
    json={"video_id": "dQw4w9WgXcQ", "length": "short", "output_lang": "en"},
    timeout=120,
)
resp.raise_for_status()
data = resp.json()

print(data.get("summary", data))
const API = "https://api.example.com/v1";
const API_KEY = process.env.TOUCORA_API_KEY ?? "YOUR_API_KEY";

const res = await fetch(`${API}/summarize`, {
  method: "POST",
  headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ video_id: "dQw4w9WgXcQ", length: "short", output_lang: "en" }),
});
if (!res.ok) throw new Error((await res.json()).error.message);

const data = await res.json();
console.log(data.summary ?? data);

MCP server

The same tools are exposed over the Model Context Protocol at https://api.example.com/mcp (Streamable HTTP, JSON-RPC 2.0). Use it from MCP clients, or call it directly. See the docs MCP section for Claude Code, Codex and Claude Desktop setup.

curl -X POST "https://api.example.com/mcp" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "get_transcript",
      "arguments": {"video_id": "dQw4w9WgXcQ", "format": "markdown"}
    }
  }'
import os
import requests

MCP_URL = "https://api.example.com/mcp"
HEADERS = {
    "Authorization": f"Bearer {os.environ['TOUCORA_API_KEY']}",
    "Content-Type": "application/json",
}

def mcp(method, params=None, request_id=1):
    payload = {"jsonrpc": "2.0", "id": request_id, "method": method}
    if params is not None:
        payload["params"] = params
    resp = requests.post(MCP_URL, headers=HEADERS, json=payload, timeout=60)
    resp.raise_for_status()
    return resp.json()

# Discover the available tools once (optional).
tools = mcp("tools/list", request_id=1)["result"]["tools"]
print([tool["name"] for tool in tools])

# Call a tool exactly like an MCP client would.
result = mcp(
    "tools/call",
    {"name": "summarize", "arguments": {"video_id": "dQw4w9WgXcQ", "length": "short"}},
    request_id=2,
)
print(result["result"]["content"][0]["text"])
const MCP_URL = "https://api.example.com/mcp";
const API_KEY = process.env.TOUCORA_API_KEY ?? "YOUR_API_KEY";

async function mcp(method, params, id = 1) {
  const res = await fetch(MCP_URL, {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", id, method, params }),
  });
  if (!res.ok) throw new Error(`MCP HTTP ${res.status}`);
  const body = await res.json();
  if (body.error) throw new Error(body.error.message);
  return body.result;
}

const tools = await mcp("tools/list", {});
console.log(tools.tools.map((tool) => tool.name).join(", "));

const result = await mcp("tools/call", {
  name: "summarize",
  arguments: { video_id: "dQw4w9WgXcQ", length: "short" },
});
console.log(result.content[0].text);

Errors

All errors share one shape and include a request_id you can quote in support requests. Common codes: INVALID_API_KEY (401), INSUFFICIENT_CREDITS (402), VIDEO_NOT_FOUND / TRANSCRIPT_NOT_AVAILABLE (404), and RATE_LIMITED (429).

{
  "error": {
    "code": "VIDEO_NOT_FOUND",
    "message": "The requested YouTube video could not be found.",
    "request_id": "req_123"
  }
}
# -i prints the status line and headers (including X-RateLimit-* on 429).
curl -i "https://api.example.com/v1/transcripts/does-not-exist" \
  -H "Authorization: Bearer YOUR_API_KEY"

# 401 invalid key · 402 insufficient credits · 404 not found · 429 rate limited
import os
import requests

API = "https://api.example.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['TOUCORA_API_KEY']}"}

resp = requests.get(f"{API}/transcripts/does-not-exist", headers=HEADERS, timeout=30)

if not resp.ok:
    error = resp.json().get("error", {})
    raise SystemExit(
        f'HTTP {resp.status_code} | {error.get("code")} | {error.get("message")} '
        f'(request_id={error.get("request_id")})'
    )
const API = "https://api.example.com/v1";
const API_KEY = process.env.TOUCORA_API_KEY ?? "YOUR_API_KEY";

const res = await fetch(`${API}/transcripts/does-not-exist`, {
  headers: { Authorization: `Bearer ${API_KEY}` },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`HTTP ${res.status} | ${error.code} | ${error.message} (request_id=${error.request_id})`);
}

Retries & backoff

Retry 429 (honouring Retry-After), 5xx responses and connection/timeout errors. Use exponential backoff with jitter and a retry cap. Never retry 400, 401, 402 or 404 — the request itself is wrong or unauthorized. Rate-limited responses also carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.

import os
import random
import time
import requests

API = "https://api.example.com/v1"
API_KEY = os.environ["TOUCORA_API_KEY"]
RETRYABLE = {429, 500, 502, 503, 504}

def backoff(attempt):
    # exponential backoff with full jitter, capped at 30s
    return random.uniform(0, min(30, 2 ** attempt))

def retry_delay(resp, attempt):
    # prefer the server's Retry-After header when present
    retry_after = resp.headers.get("Retry-After")
    if retry_after:
        try:
            return float(retry_after)
        except ValueError:
            pass
    return backoff(attempt)

def call_api(method, path, max_retries=5, **kwargs):
    headers = {"Authorization": f"Bearer {API_KEY}", **kwargs.pop("headers", {})}
    for attempt in range(max_retries + 1):
        try:
            resp = requests.request(method, f"{API}{path}", headers=headers, timeout=60, **kwargs)
        except (requests.ConnectionError, requests.Timeout):
            if attempt == max_retries:
                raise
            delay = backoff(attempt)
        else:
            if resp.status_code not in RETRYABLE:
                if resp.status_code >= 400:
                    error = resp.json().get("error", {})
                    raise RuntimeError(f'{resp.status_code} {error.get("code")}: {error.get("message")}')
                return resp
            if attempt == max_retries:
                resp.raise_for_status()
            delay = retry_delay(resp, attempt)
        print(f"retry {attempt + 1}/{max_retries} in {delay:.1f}s")
        time.sleep(delay)

transcript = call_api("GET", "/transcripts/dQw4w9WgXcQ", params={"lang": "en"}).json()
print(len(transcript["segments"]), "segments")
const API = "https://api.example.com/v1";
const API_KEY = process.env.TOUCORA_API_KEY ?? "YOUR_API_KEY";
const RETRYABLE = new Set([429, 500, 502, 503, 504]);

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const backoff = (attempt) => Math.random() * Math.min(30_000, 2 ** attempt * 500);

async function callApi(path, { method = "GET", body, maxRetries = 5 } = {}) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    let res;
    try {
      res = await fetch(`${API}${path}`, {
        method,
        headers: {
          Authorization: `Bearer ${API_KEY}`,
          ...(body ? { "Content-Type": "application/json" } : {}),
        },
        body: body ? JSON.stringify(body) : undefined,
      });
    } catch (err) {
      if (attempt === maxRetries) throw err;
      await sleep(backoff(attempt));
      continue;
    }

    if (!RETRYABLE.has(res.status)) {
      if (!res.ok) {
        const { error } = await res.json().catch(() => ({ error: {} }));
        throw new Error(`${res.status} ${error.code}: ${error.message}`);
      }
      return res;
    }

    if (attempt === maxRetries) throw new Error(`gave up after ${maxRetries} retries (HTTP ${res.status})`);
    const retryAfter = Number(res.headers.get("Retry-After"));
    await sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : backoff(attempt));
  }
}

const res = await callApi("/transcripts/dQw4w9WgXcQ?lang=en");
const transcript = await res.json();
console.log(transcript.segments.length, "segments");
# curl retries connection failures and 5xx responses, and honours a
# Retry-After header automatically. --retry-all-errors also covers 429.
curl --retry 5 --retry-delay 1 --retry-connrefused --retry-all-errors \
  --fail-with-body \
  "https://api.example.com/v1/transcripts/dQw4w9WgXcQ?lang=en" \
  -H "Authorization: Bearer YOUR_API_KEY"

# For full control, loop with exponential backoff in a shell script.
attempt=0
until curl -sS --fail-with-body "https://api.example.com/v1/summarize" \
    -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
    -d '{"video_id":"dQw4w9WgXcQ","length":"short"}' -o response.json; do
  attempt=$((attempt + 1))
  [ "$attempt" -ge 5 ] && { echo "giving up after $attempt attempts"; exit 1; }
  sleep $((2 ** attempt))
done
cat response.json

Want runnable files instead of browser snippets? The same examples live in the examples/ directory of the project repository, split by language. Official SDKs for JavaScript, Python and Go are on the roadmap after the API stabilizes.