Skip to API reference

The YouTube transcript API, ready for code.

Extract transcripts, inspect whole channels, or analyze the visual evidence inside a video. One REST API, predictable JSON, and no scraping infrastructure to maintain.

Building for an AI assistant? Connect through MCP and skip the client code.

First request REST · JSON
curl -X POST https://vidwords.com/api/transcripts \
  -H "Authorization: Basic YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids":["dQw4w9WgXcQ"],"lang":"en"}'
200 { "results": [{ "text": "…", "segments": […] }] }
Base URL
https://vidwords.com
Authentication
Basic token
Response format
JSON
Free account
Developer trial

Make your first request

Three checks prevent nearly every first-call error.

  1. 1
    Create a key

    On the API & MCP page — its value is shown once, when you create it. API access is included on every plan.

  2. 2
    Verify your email

    Open the confirmation link before making your first call.

  3. 3
    Send a video ID

    Use a YouTube ID or full URL. The response includes text and timestamps.

Authentication

Send your API token in the Authorization header of every request:

Authorization: Basic <your-api-token>
Content-Type: application/json
Confirm your email first.Until you click the verification link we sent you, every API call returns 403 with {"error":"email_unverified"}. This is the most common first-call failure on a new account.

Rate limits & usage

Request ceiling
60/min on Free, 200 on Starter, 500 on Pro, and 1,000 on Team.
Delivered transcript
1 Cloud Request. Failed lookups, including no captions or an invalid ID, are free.
Rate limit reached
429 rate_limited. Wait for the Retry-After duration before retrying.
Pacing headers
Every response that reaches the rate limiter carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until a request frees up), so you can slow down before a 429. A request refused before that point, such as a missing or invalid token, an unconfirmed email or a body that could not be read, has none. On /api/watch they describe that endpoint’s own, tighter limit. A 429 from the per-plan ceiling that /api/watch shares with the rest of the API carries that ceiling’s headers instead.
Balance too low
402. The request stops before processing and consumes nothing.
Captionless videosA fresh transcript also uses 3 AI Units per audio minute and needs a paid plan or an AI Unit top-up; the Free monthly allowance does not cover audio transcription. On paid plans, a result that fails our quality check gets a second transcription pass. Replays cost the usual 1 Cloud Request without a fresh ASR charge.
POST

/api/transcripts

Fetch timestamped transcripts for as many as 50 videos in one request.

ids accepts video IDs or full YouTube URLs. Optional lang sets the preferred caption language and falls back to whatever the video has.

Transcript request and response fields
LocationFieldMeaning
Requestids RequiredArray of 1–50 video IDs or full YouTube URLs.
RequestlangPreferred caption language code. Omit it for the video’s own language; pass en for English.
RequesttranscribeAudioDefault true: a video with no captions is transcribed from its audio, for 3 AI Units per minute of video plus the Cloud Request, charged only when a transcript is delivered. Send false to refuse it; the item then carries audioTranscriptionOffer with the video’s length and estimatedAiUnits, and nothing is charged.
RequestmaxAiUnitsThe most AI Units any one video’s audio transcription may cost. A dearer video is refused before anything is downloaded, as asr_insufficient with its price, and is not charged.
ResponseresultsOne transcript or per-video error for every requested ID.
ResponsecloudRequestsRemainingCloud Requests available after this call.
ResponseaiUnitsRemainingAI Processing Units available after this call.
curl -X POST https://vidwords.com/api/transcripts \
  -H "Authorization: Basic YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["dQw4w9WgXcQ", "https://youtu.be/jNQXAC9IVRw"], "lang": "en"}'
const res = await fetch("https://vidwords.com/api/transcripts", {
  method: "POST",
  headers: {
    "Authorization": "Basic YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ ids: ["dQw4w9WgXcQ", "jNQXAC9IVRw"], lang: "en" }),
});
const data = await res.json();
console.log(data.results);
import requests

res = requests.post(
    "https://vidwords.com/api/transcripts",
    headers={
        "Authorization": "Basic YOUR_API_TOKEN",
        "Content-Type": "application/json",
    },
    json={"ids": ["dQw4w9WgXcQ", "jNQXAC9IVRw"], "lang": "en"},
)
print(res.json()["results"])

Transcript provenance: the optional source field describes the text as captions, auto_captions, transcribed or browser_capture. Use it instead of the legacy method marker. Provider details and internal quality measurements are not part of the customer response.

Compatibility update · October 17, 2026: existing audio and browser method markers remain available for installed clients; caption retrieval methods are grouped together. New integrations should use source.

Response:

{
  "results": [
    {
      "id": "dQw4w9WgXcQ",
      "title": "…",
      "author": "…",
      "language": "English",
      "languageCode": "en",
      "isGenerated": false,
      "text": "full transcript as one string…",
      "segments": [{ "text": "…", "start": 1.36, "duration": 1.68 }, …]
    },
    { "id": "…", "error": "no_transcript", "message": "No transcript is available for this video" }
  ],
  "cloudRequestsRemaining": 123,
  "aiUnitsRemaining": 580
}
POST

/api/channels Starter & up

Resolve channels into structured upload lists.

Available on Starter (up to 10 channels/request), Pro (100), and Team (500). ids accepts @handles, channel URLs, or UC… channel IDs. Each successful channel listing uses one Cloud Request; retrieving transcripts uses one more per delivered video.

Channel request and response fields
LocationFieldMeaning
Requestids RequiredChannel handles, URLs, or UC… IDs; the batch limit depends on your plan.
ResponseresultsOne channel result or per-channel error for every requested ID.
ResponsevideosNewest uploads with each video’s ID, title, and author.
ResponsevideoCountNumber of uploads found for the channel.
Responsetruncatedtrue when more uploads exist beyond the returned list.
curl -X POST https://vidwords.com/api/channels \
  -H "Authorization: Basic YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["@mkbhd"]}'
const res = await fetch("https://vidwords.com/api/channels", {
  method: "POST",
  headers: {
    "Authorization": "Basic YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ ids: ["@mkbhd"] }),
});
console.log((await res.json()).results);
import requests

res = requests.post(
    "https://vidwords.com/api/channels",
    headers={"Authorization": "Basic YOUR_API_TOKEN", "Content-Type": "application/json"},
    json={"ids": ["@mkbhd"]},
)
print(res.json()["results"])

Response:

{
  "results": [
    {
      "id": "@mkbhd",
      "channelId": "UCBJycsmduvYEL83R_U4JriQ",
      "title": "…",
      "videoCount": 500,
      "truncated": true,
      "videos": [{ "id": "…", "title": "…", "author": "…" }, …]
    }
  ]
}
POST

/api/watch Video evidence

Analyze speech and on-screen evidence with timestamped citations.

Everything above reads captions. This reads the picture — slides, charts, demos, on-screen text a transcript never contains — and returns evidence tied to exact timestamps.

The part worth paying for is what it refuses to say. Every citation is checked against the stored analysis before you see it: a visual claim must match a real recorded frame, a spoken one a real transcript segment. When nothing survives the check, the answer says the evidence is insufficient instead of guessing.

Analysis takes minutes, so this is asynchronous — start a job, then poll it. Standard Watch uses 4 AI Processing Units per minute of video; Deep Watch uses 30.

Start an analysis

curl -X POST https://vidwords.com/api/watch \
  -H "Authorization: Basic YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id": "dQw4w9WgXcQ", "mode": "smart"}'

# -> 202 {"jobId": 1234, "status": "queued", ...}
# (this account already ran the same analysis — same video, mode and range?
#  then you get that job back free: 200 if it is ready, 202 while it still runs)
# (already analysed for someone else? -> 200 {"status": "ready", "cached": true, ...}
#  at once, and no watch.completed webhook follows: read the result from this response)
const auth = { "Authorization": "Basic YOUR_API_TOKEN" };

const start = await fetch("https://vidwords.com/api/watch", {
  method: "POST",
  headers: { ...auth, "Content-Type": "application/json" },
  body: JSON.stringify({ id: "dQw4w9WgXcQ", mode: "smart" }),
});
// Check this. A refusal (402 out of minutes, 422 too long, 503 unavailable)
// returns {error, message} and no jobId — polling it would just 404 forever.
if (!start.ok) {
  const { error, message } = await start.json();
  throw new Error(`${error}: ${message}`);
}
const { jobId } = await start.json();

// Poll until ready — usually a few minutes.
let job;
do {
  await new Promise((r) => setTimeout(r, 10_000));
  job = await (await fetch(`https://vidwords.com/api/watch/${jobId}`, { headers: auth })).json();
} while (job.status === "queued" || job.status === "processing");

if (job.status === "failed") throw new Error(job.error ?? "analysis failed");
console.log(job.artifact);
AI Watch request fields
FieldMeaning
idYouTube video URL or id. Public videos only.
modequick, smart (default), deep, or auto. auto picks one from the video’s length and visual pace; deep costs 30 AI Processing Units per minute and runs on any plan with enough of them.
langPreferred caption language. Omit it for the video’s own language; pass en for English.
outputLangThe language the analysis is written in, such as es or pt-BR. Omit it to write in the video’s own language. No extra charge; quoted captions and on-screen text stay exactly as they appear.
partialIf the video is longer than your plan allows, analyze the first allowed minutes and charge only for those. Never implied — you have to ask.

GET/api/watch/:jobId

Poll this until status is ready, then read artifact — the summary, chapters and timestamped evidence. Analyses from October 2026 also carry brief (the TL;DR, each bullet with the second that supports it), facts (every figure the video states, verbatim, with where it is stated), videoKind and followUps; older ones carry keyPoints instead. Treat every one of those as optional. While queued or processing there is no artifact yet. A failed job carries errorCode and retryable. Free.

POST/api/watch/:jobId/ask

Ask a question against a finished analysis. Returns the answer with its verified citations, or says the evidence is insufficient. Uses 6 AI Processing Units.

curl -X POST https://vidwords.com/api/watch/1234/ask \
  -H "Authorization: Basic YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"question": "What database did they benchmark against?"}'

Good to know

  • Starting the same analysis twice does not charge twice. A run of the same video, mode and range already queued, running or finished on your account comes back as that same job.
  • Standard Watch uses 4 AI Processing Units per minute of video; Deep Watch uses 30. Free includes 200 AI Units/month, Starter 3,000, Pro 10,000 and Team 28,000, followed by any purchased AI top-up balance.
  • A run refused before it starts — video not public, age-restricted, too long, out of minutes — costs nothing.
  • Start is limited to 10 requests/minute and polling to 30 per 10 seconds, so a polling loop can’t lock you out of starting the next job.
  • Agents can reach all of this through the MCP server as analyze_video, get_analysis and ask_video.
POST

/mcp MCP server

Give an AI assistant direct, authenticated access to VidWords tools.

Point Claude, ChatGPT, Cursor, or any MCP-compatible agent at https://vidwords.com/mcp and it can read and search YouTube videos on its own — same API token, same Cloud and AI balances, no glue code to write.

The tool worth knowing about is search_transcript. It returns the moments matching your question with timestamps and youtube.com/watch?v=…&t=…s links already attached, so the assistant answers with a citation you can click instead of pasting the whole transcript at you.

Connect it

claude mcp add --transport http vidwords https://vidwords.com/mcp \
  --header "Authorization: Basic YOUR_API_TOKEN"
// claude_desktop_config.json — Desktop runs only local servers, so
// bridge the remote one with mcp-remote (or add it under Customize →
// Connectors, which signs in with no key at all). The key goes in env:
// Claude Desktop on Windows does not escape spaces inside args.
{
  "mcpServers": {
    "vidwords": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://vidwords.com/mcp",
               "--header", "Authorization:${VIDWORDS_MCP_AUTH}"],
      "env": { "VIDWORDS_MCP_AUTH": "Basic YOUR_API_TOKEN" }
    }
  }
}
# .cursor/mcp.json
{
  "mcpServers": {
    "vidwords": {
      "url": "https://vidwords.com/mcp",
      "headers": { "Authorization": "Basic YOUR_API_TOKEN" }
    }
  }
}

Tools

MCP tools and usage costs
ToolWhat it doesCost
search_transcriptFind where a video discusses something. Takes one video or a list of up to 25, so one call can search a whole channel. Returns the matching moments with timestamps, quoted context, and youtube.com/watch?v=…&t=…s deep links.1 Cloud Request per new video
get_transcriptTranscript text for up to 25 videos, or the span between two timecodes. maxChars pages a long one, lang picks a caption track, source: "audio" transcribes the speech itself.1 Cloud Request per new video
translate_transcriptOne video’s transcript translated into any language by AI, line for line, every timestamp kept — for a language the video has no captions in, or an audio transcription. estimateOnly quotes the price first; reading it again is free.2 AI Units per started minute of video, only when it succeeds
search_librarySearch every transcript you have already saved, with timestamps and deep links.Free
list_libraryYour saved videos, newest first.Free
list_languagesA video's caption tracks, and which are auto-generated (the speech) versus uploaded (possibly translations).Free
list_channel_videosResolve a channel handle, URL or UC… id to its recent uploads.1 Cloud Request · Starter and up
list_watchlistsYour Radar watchlists and how much each has recorded.Free
watchlist_activityNewest uploads Radar has recorded for one watchlist.Free
create_watchlistCreate a named Radar watchlist. Automatic analysis starts off.Free
add_watchlist_channelMonitor a channel URL, @handle or UC… id on a watchlist, up to 25 channels each.Free
check_watchlistCheck a watchlist's channels for new uploads now. Never starts a paid analysis.Free
update_watchlistRename or pause a watchlist, set digest emails, title include/exclude phrases and monthly analysis ceilings. Turning on automatic analysis spends AI Units at the usual Watch rates.Free to change; analysis is billed
preview_watchlist_filtersTest unsaved title phrases against the latest 20 recorded uploads without saving anything.Free
remove_watchlist_channelStop monitoring one channel and delete its recorded history on that watchlist.Free
delete_watchlistDelete a watchlist with its channels and recorded videos.Free
set_radar_pausedPause or resume all Radar monitoring and digest emails for the account.Free
accountPlan and remaining Cloud and AI balances, so the agent can price a job before running it.Free
analyze_videoStart a frame-level analysis — slides, charts, demos and on-screen text, not just captions. Returns an analysisId immediately; estimateOnly: true quotes the price instead, without starting anything. A video with no captions is analyzed from its soundtrack unless transcribeAudio: true asks for a word-for-word transcript too.1 Cloud Request, then AI Units per minute: Standard analysis 4, Deep 30, Quick 2.8; estimate free
list_analysesYour AI Watch analyses, newest first, with each analysisId, video, mode and status. Filters by video.Free
get_analysisRead a finished analysis: chapters, key points, timestamped evidence. waitSeconds (up to 25) holds the call until it is ready instead of polling.Free
ask_videoAsk a question against a finished analysis. Citations are verified against stored evidence or dropped.6 AI Units per question

Re-reading a video you already have is free — another time range, another page, or a search after a fetch — and every metered result reports what it charged.

Good to know

  • Prefer search_transcript over get_transcript. Both cost the same, but a full transcript of a two-hour video fills the assistant's context with material irrelevant to your question. Search returns only the parts that answer it.
  • Ask for a time span instead of a whole video. Both transcript tools take optional from and to timecodes — seconds (615), m:ss (10:20) or h:mm:ss (1:02:13). It's the same format the tools print back, so a timestamp from one answer can be pasted straight into the next question.
  • The MCP endpoint uses the same plan-based request limit as REST: 60 requests/minute on Free, 200 on Starter, 500 on Pro, and 1,000 on Team. Starting an analysis has its own 10 requests/minute safety limit.
  • Cloud and AI balances work exactly as they do over REST: one Cloud Request per delivered transcript, AI Units for analysis, nothing for a failed lookup, and a readable error before a job exceeds its balance.
  • Ran out mid-conversation? The assistant gets a readable error telling it to check account or top up, rather than an opaque failure it will just retry.
  • Clients that support MCP sign-in need no token at all; anything headless sends a VidWords API key (Basic), created on the API & MCP page. RapidAPI keys don't work here.
POST

Webhooks Pro & Team

Get told when a transcript or an analysis finishes, instead of polling for it.

Add an HTTPS endpoint on the API & MCP page and VidWords sends it a signed POST for every event below. Pro allows 5 endpoints and Team 20. The endpoint must be a public https:// URL on port 443. Its signing secret (whsec_…) is shown once, when you create it.

Webhook events and their data
EventWhendata fields
transcript.completedA transcript reached your account, from the website, the extension, the API or MCP.videoId, source, librarySaved; a background job adds jobId.
transcript.failedA queued background transcript job failed. A direct API call reports its failure in the response and sends no event.jobId, videoId, source, error, retryable
watch.completedAn AI Watch analysis that was still running when you started it is ready. Read it with GET /api/watch/:jobId. Not sent when the start itself answered 200 with a ready job (see below).jobId, videoId, mode, origin (user or radar), status: "ready"
watch.failedAn AI Watch analysis failed. Its AI Units were refunded.Same as watch.completed, with status: "failed"
A Watch start can already be finished.When the analysis you ask for has been produced before, for this account or for anyone else (same video, mode, range and output language), POST /api/watch answers 200 with status: "ready" straight away and no watch.completed event is sent for it. Nothing finished later, so there is nothing to announce. Handle the ready response directly; an integration that only waits for the webhook will wait forever on these.

Every delivery has the same envelope:

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: VidWords-Webhooks/1.0
X-VidWords-Event: watch.completed
X-VidWords-Delivery: evt_3f9c1a7b2d4e6f80a1b2c3d4e5f60718
X-VidWords-Signature: t=1760261400,v1=5d41402abc4b2a76b9719d911017c592ae2f5c1c6e2b8d1b5f4a3c2e1d0f9a8b

{
  "id": "evt_3f9c1a7b2d4e6f80a1b2c3d4e5f60718",
  "type": "watch.completed",
  "createdAt": "2026-10-12T09:30:00.000Z",
  "data": { "jobId": 1234, "videoId": "dQw4w9WgXcQ", "mode": "smart", "origin": "user", "status": "ready" }
}

Verify the signature

X-VidWords-Signature is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw request body>" keyed with your endpoint’s signing secret. Compute it over the body exactly as it arrived, before any JSON parsing, and compare in constant time.

import crypto from "node:crypto";
import http from "node:http";

const SECRET = process.env.VIDWORDS_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_SECONDS = 300; // refuse anything signed more than 5 minutes away

function verify(rawBody, header) {
  const parts = Object.fromEntries(
    String(header ?? "").split(",").map((pair) => pair.trim().split("="))
  );
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !/^[0-9a-f]{64}$/.test(parts.v1 ?? "")) return false;
  if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
  const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${rawBody}`).digest();
  return crypto.timingSafeEqual(expected, Buffer.from(parts.v1, "hex"));
}

const seen = new Set(); // use your database in production

http.createServer((req, res) => {
  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => {
    const raw = Buffer.concat(chunks).toString("utf8");
    if (!verify(raw, req.headers["x-vidwords-signature"])) {
      res.writeHead(400).end();
      return;
    }
    res.writeHead(204).end(); // acknowledge first, then do the work
    const event = JSON.parse(raw);
    if (seen.has(event.id)) return; // a retry of an event you already handled
    seen.add(event.id);
    console.log(event.type, event.data);
  });
}).listen(8080);

Retries and replays

  • Answer with any 2xx within 10 seconds. Anything else counts as a failure: another status, a timeout, or a redirect, which is not followed.
  • Failed deliveries are retried 10 seconds, 1 minute, 5 minutes and 30 minutes after each failed attempt, so five attempts in all over about 36 minutes. After the fifth, the delivery is marked failed.
  • Delivery is at least once and unordered. A retry carries the same id and X-VidWords-Delivery, so de-duplicate on it.
  • Refuse stale signatures. Each attempt is signed afresh with the current time, so a 5-minute tolerance on t rejects a captured request replayed later without ever rejecting a genuine retry.
  • Pausing an endpoint stops new events reaching it. If your plan stops including webhooks, pending deliveries are stopped too.

Automation tools

Start with an importable workflow instead of wiring each request yourself.

Plug VidWords into no-code automation platforms — import a template, set your API token, and you're running.

n8n workflow

Ready-to-use n8n workflow for automated transcript extraction. Import and configure with your API token.

Download

Make blueprint

Ready-to-use Make.com blueprint for automated transcript extraction. Import and configure with your API token.

Download

Errors and recovery

Errors use stable codes so your integration can choose the next action.

HTTP errors and recommended recovery
HTTPCodeMeaning
400bad_requestMalformed body, too many ids, or invalid input
401unauthorizedMissing or invalid API token
402insufficient_cloud_requests / insufficient_ai_units / insufficient_creditsNot enough of the required balance (insufficient_credits is POST /api/transcripts: the batch is larger than the Cloud Requests left). The body also carries topUpUrl, and on a monthly plan autoRechargeUrl: turn on auto-recharge there and the balance refills within a monthly cap you set, so a script does not stop
403email_unverifiedOpen the confirmation link we emailed you, then retry
403plan_requiredEndpoint needs a higher plan (channels API)
429rate_limitedToo many requests — honor Retry-After
403unsupported_transport/mcp and /api/watch — a RapidAPI key was used. Use a VidWords API token.
500internalSomething failed on our side. Retry after a short wait
502provider codePOST /api/watch/:jobId/ask — the answer could not be produced; the question was refunded
503not_configured / free_capacityAI Watch is unavailable, or today’s free capacity is used up (resets at 00:00 UTC)

On /mcp, problems the assistant can act on — no balance left, plan too low, bad video id — come back inside a successful tool result rather than as a transport error, so it can tell you what went wrong instead of silently retrying.

Per-video errors inside results: invalid_id, no_transcript, transcripts_disabled, video_unavailable, age_restricted, video_unplayable, ip_blocked, request_blocked, fetch_failed, internal. Per-channel errors: invalid_id, channel_not_found, internal.