Make your first request
Three checks prevent nearly every first-call error.
- 1Create a key
On the API & MCP page — its value is shown once, when you create it. API access is included on every plan.
- 2Verify your email
Open the confirmation link before making your first call.
- 3Send 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
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 theRetry-Afterduration before retrying.- Pacing headers
- Every response that reaches the rate limiter carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset(seconds until a request frees up), so you can slow down before a429. 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/watchthey describe that endpoint’s own, tighter limit. A429from the per-plan ceiling that/api/watchshares with the rest of the API carries that ceiling’s headers instead. - Balance too low
402. The request stops before processing and consumes nothing.
/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.
| Location | Field | Meaning |
|---|---|---|
| Request | ids Required | Array of 1–50 video IDs or full YouTube URLs. |
| Request | lang | Preferred caption language code. Omit it for the video’s own language; pass en for English. |
| Request | transcribeAudio | Default 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. |
| Request | maxAiUnits | The 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. |
| Response | results | One transcript or per-video error for every requested ID. |
| Response | cloudRequestsRemaining | Cloud Requests available after this call. |
| Response | aiUnitsRemaining | AI Processing Units available after this call. |
Need generated types or every nested field? Use the exhaustive OpenAPI schema.
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
}
/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.
| Location | Field | Meaning |
|---|---|---|
| Request | ids Required | Channel handles, URLs, or UC… IDs; the batch limit depends on your plan. |
| Response | results | One channel result or per-channel error for every requested ID. |
| Response | videos | Newest uploads with each video’s ID, title, and author. |
| Response | videoCount | Number of uploads found for the channel. |
| Response | truncated | true when more uploads exist beyond the returned list. |
Need generated types or every nested field? Use the exhaustive OpenAPI schema.
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": "…" }, …]
}
]
}
/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);
| Field | Meaning |
|---|---|
id | YouTube video URL or id. Public videos only. |
mode | quick, 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. |
lang | Preferred caption language. Omit it for the video’s own language; pass en for English. |
outputLang | The 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. |
partial | If 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_analysisandask_video.
/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
| Tool | What it does | Cost |
|---|---|---|
search_transcript | Find 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_transcript | Transcript 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_transcript | One 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_library | Search every transcript you have already saved, with timestamps and deep links. | Free |
list_library | Your saved videos, newest first. | Free |
list_languages | A video's caption tracks, and which are auto-generated (the speech) versus uploaded (possibly translations). | Free |
list_channel_videos | Resolve a channel handle, URL or UC… id to its recent uploads. | 1 Cloud Request · Starter and up |
list_watchlists | Your Radar watchlists and how much each has recorded. | Free |
watchlist_activity | Newest uploads Radar has recorded for one watchlist. | Free |
create_watchlist | Create a named Radar watchlist. Automatic analysis starts off. | Free |
add_watchlist_channel | Monitor a channel URL, @handle or UC… id on a watchlist, up to 25 channels each. | Free |
check_watchlist | Check a watchlist's channels for new uploads now. Never starts a paid analysis. | Free |
update_watchlist | Rename 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_filters | Test unsaved title phrases against the latest 20 recorded uploads without saving anything. | Free |
remove_watchlist_channel | Stop monitoring one channel and delete its recorded history on that watchlist. | Free |
delete_watchlist | Delete a watchlist with its channels and recorded videos. | Free |
set_radar_paused | Pause or resume all Radar monitoring and digest emails for the account. | Free |
account | Plan and remaining Cloud and AI balances, so the agent can price a job before running it. | Free |
analyze_video | Start 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_analyses | Your AI Watch analyses, newest first, with each analysisId, video, mode and status. Filters by video. | Free |
get_analysis | Read a finished analysis: chapters, key points, timestamped evidence. waitSeconds (up to 25) holds the call until it is ready instead of polling. | Free |
ask_video | Ask 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_transcriptoverget_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
fromandtotimecodes — seconds (615),m:ss(10:20) orh: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
accountor 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.
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.
| Event | When | data fields |
|---|---|---|
transcript.completed | A transcript reached your account, from the website, the extension, the API or MCP. | videoId, source, librarySaved; a background job adds jobId. |
transcript.failed | A 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.completed | An 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.failed | An AI Watch analysis failed. Its AI Units were refunded. | Same as watch.completed, with status: "failed" |
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
idandX-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
trejects 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.
Every event’s schema is in the webhooks section of the OpenAPI specification.
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.
Errors and recovery
Errors use stable codes so your integration can choose the next action.
| HTTP | Code | Meaning |
|---|---|---|
400 | bad_request | Malformed body, too many ids, or invalid input |
401 | unauthorized | Missing or invalid API token |
402 | insufficient_cloud_requests / insufficient_ai_units / insufficient_credits | Not 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 |
403 | email_unverified | Open the confirmation link we emailed you, then retry |
403 | plan_required | Endpoint needs a higher plan (channels API) |
429 | rate_limited | Too many requests — honor Retry-After |
403 | unsupported_transport | /mcp and /api/watch — a RapidAPI key was used. Use a VidWords API token. |
500 | internal | Something failed on our side. Retry after a short wait |
502 | provider code | POST /api/watch/:jobId/ask — the answer could not be produced; the question was refunded |
503 | not_configured / free_capacity | AI 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.