Search YouTube by keyword and get back up to 25 lightweight results — url, title, uploader, durationSec, thumbnail, views — with no login and no video download. It's the missing first step for an agent that only ever gets handed a URL: now it can start from a topic instead. Flat $0.002 per call, YouTube only.
Every FrameFetch endpoint before this one needed a URL to start from — fine when a human hands one over, useless when an agent is working from a topic instead ("find me a review of the Framework 16" has no URL in it anywhere). POST /v1/search closes that loop: give it a keyword, get back candidate videos, then call /v1/extract on whichever one is relevant. A search-only tool and an extract-only tool are each half a workflow; together they're the whole thing, in two calls.
Send query (2–200 characters) and an optional limit (1–25, default 10) to POST /v1/search. No frames spec, no fields array — search has its own small, fixed shape. Under the hood it's one yt-dlp search pass: no per-video extraction, no media download, no LLM call.
curl -X POST https://framefetch.net/v1/search \
-H "Authorization: Bearer <your-key>" \
-H "Content-Type: application/json" \
-d '{
"query": "framework 16 laptop review",
"limit": 3
}'{
"platform": "youtube",
"query": "framework 16 laptop review",
"results": [
{ "url": "https://www.youtube.com/watch?v=...", "title": "Framework 16 Review: Just Josh", "uploader": "Just Josh", "durationSec": 1042, "uploadDate": null, "thumbnail": "https://i.ytimg.com/...", "views": 214830 },
{ "url": "https://www.youtube.com/watch?v=...", "title": "Framework 16 Review", "uploader": "Linus Tech Tips", "durationSec": 891, "uploadDate": null, "thumbnail": "https://i.ytimg.com/...", "views": 1820442 },
{ "url": "https://www.youtube.com/watch?v=...", "title": "Framework 16 Long Term Review", "uploader": "Garett", "durationSec": 763, "uploadDate": null, "thumbnail": "https://i.ytimg.com/...", "views": 58211 }
],
"cost": { "totalMicros": 2000 }
}This exact query — "framework 16 laptop review", limit 3 — is a real, measured call: 3 results back in about 2.7 seconds (Just Josh, Linus Tech Tips, and Garett, in that order), each with a channel name and duration. Read that as one observation, not a benchmark — a single timed call, not a latency distribution. Titles, view counts, and thumbnail URLs shown here are illustrative of the shape, not re-copied verbatim from that run.
| Field | Shape | What it captures |
|---|---|---|
url | string | The video's watch URL — feed this straight into /v1/extract |
title | string | null | Video title, as yt-dlp's search returned it |
uploader | string | null | Channel / uploader name |
durationSec | number | null | Video length in seconds |
uploadDate | string | null | Null in practice. The field exists in the response shape, but yt-dlp's flat search extractor does not report it — expect null on nearly every real result, not an occasional gap. If you need a real upload date, get it from /v1/extract's metadata on the video's URL instead. |
thumbnail | string | null | Thumbnail image URL |
views | number | null | View count at search time |
Every field comes straight from yt-dlp's flat-search JSON — nothing is invented, and nothing is back-filled by a second per-video lookup (that's what /v1/extract is for). results is ordered exactly as yt-dlp's search ranked them; FrameFetch never re-sorts. A search with no matches is a normal 200 with results: [], not an error.
Search alone gets you a shortlist. The useful move is chaining it into /v1/extract's ask param on the result that looks right — one call to find candidates, one to get a direct answer from the best match, grounded in its transcript with timestamped quotes instead of you reading anything yourself:
curl -X POST https://framefetch.net/v1/extract \
-H "Authorization: Bearer <your-key>" \
-H "Content-Type: application/json" \
-d '{
"url": "<url from the /v1/search result above>",
"ask": "Does the reviewer recommend it for programming?"
}'"ask": { "answer": "Yes — the reviewer says it handles day-to-day coding comfortably, with the usual caveats about battery life under heavy compile loads.", "quotes": [ { "t_sec": 312, "text": "for coding and writing, this thing is honestly great" } ], "confidence": "medium", "based_on": ["transcript"] }
The ask object above is a schema illustration, not a re-run of the search example — it hasn't been executed against a specific video for this page. What's real is the shape: { answer, quotes, confidence, based_on }, exactly as documented on the question answering page. Two calls, $0.002 + $0.0075 = $0.0095 total, and you went from a topic to a sourced answer without reading a single transcript yourself.
Built for a person scrolling and clicking. There's no stable JSON shape to parse, no per-call cost to budget against, and no clean way to hand a result straight to a second tool call — a program has to scrape a rendered page to get anything structured out of it at all.
/v1/search
{ platform, query, results[], cost } — no scraping, no HTML to parse$0.002 per call regardless of limit, itemized in the same cost block every FrameFetch call returnsurl plugs directly into /v1/extract — same auth, same account, one integration200, not something to catch as an errorOne lightweight yt-dlp search pass, regardless of how many results come back — that's why it's priced flat rather than per-result: $0.002 per call, the same price floor a bare metadata-only extract costs. No LLM call, no video download, whether limit is 1 or 25. See the full rate card.
Identical query + limit + platform calls hit the same content-hash result cache /v1/extract uses — a repeat search for the same keywords is served from cache at the same $0.002 price floor rather than re-running yt-dlp. Pass "cache": false to force a fresh search (billed the same either way, since $0.002 is already the floor) and re-populate the cache for the next caller.
Search runs on YouTube only, because that's the one platform where yt-dlp's search extractor actually works. platform defaults to "youtube" and accepts no other value today — sending anything else is a typed 400, not a silent fallback or a partial result. A genuine yt-dlp failure (every attempt, proxy and no-proxy, threw) is a distinct 502 SEARCH_FAILED — kept separate from a zero-result search, which is always a normal 200.
FrameFetch ships an MCP server at POST https://framefetch.net/mcp. The framefetch_search tool takes the same query/limit/platform arguments as the HTTP API — identical behavior, so an agent working from a topic instead of a URL has a tool for that first step, not just for the ones after it.
Up to limit (1-25, default 10) YouTube results, each with url, title, uploader, durationSec, thumbnail, and views — exactly what yt-dlp's search extractor returns, nothing invented or backfilled. uploadDate is also in the shape but is null in practice: yt-dlp's flat search does not return it, so treat it as effectively unpopulated rather than relying on it.
A flat $0.002 per call regardless of limit or how many results actually came back, including a genuine zero-result search (a normal 200, not an error). That is the same floor a bare metadata-only extract costs. There is no LLM call and no video download involved in a search — it is a lightweight yt-dlp search pass.
YouTube only. That is the one platform where yt-dlp's search extractor works; any other platform value in the request is a typed 400, not a silent fallback.
Take a result's url from the search response and call POST /v1/extract on it for metadata, transcript, frames, or on-screen text — or add a top-level ask question to that same extract call and get a grounded answer with timestamped quotes instead of parsing anything yourself. Two calls: one to find the video, one to read or question it.
Yes. Identical query + limit + platform calls are served from the same content-hash result cache /v1/extract uses — a repeat search costs the same $0.002 floor either way, cached or fresh.