Docs
One social-video URL in → metadata, transcript, insights, frames, and on-screen text out. REST + MCP. Everything on this page to make your first call.
🤖 Agent quick-start · Auth · Extract · Scoped endpoints · Search · Frames spec · Text overlay (OCR) · Batch · Digest · Video delta · Markdown · Channel monitor · Video pages · Response · Errors · MCP · Paying (x402) · Platforms · Account & usage · Billing · Translation & subtitles · Audio briefings · Structured video understanding · Comments & audience sentiment · Ask (question answering)
Hand this to your AI agent
Too lazy to read the docs? Copy the prompt below and paste it to your coding agent (Claude Code, Cursor, Codex, Hermes — any tool). It gets a free key, wires up MCP or REST for the tool it's running in, and runs a test call — no further setup from you.
Set up FrameFetch for me and verify it works. FrameFetch is an agent-first API that turns any social-video URL (YouTube, Shorts, TikTok, Instagram Reels, Pinterest, Reddit) into metadata, transcript (25-language translation, SRT/VTT), frames, on-screen text (OCR), spoken audio briefings, and structured understanding (chapters/entities/claims/sentiment). Do the whole setup yourself, end to end:
1) GET A FREE API KEY (no card, 100 free calls/month). Ask me for my email, then mint the key:
curl -X POST https://framefetch.net/v1/keys -H "Content-Type: application/json" -d '{"email":"MY_EMAIL"}'
The JSON response has "key" (starts with ff_). Store it as the env var FRAMEFETCH_API_KEY / in my secrets — never hardcode it. I can re-view or rotate it anytime at https://framefetch.net/account.
2) WIRE IT UP for whatever tool you are running in (pick ONE):
- MCP (preferred). Remote streamable-HTTP endpoint: https://framefetch.net/mcp
* Claude Code: claude mcp add framefetch --transport http https://framefetch.net/mcp --header "Authorization: Bearer $FRAMEFETCH_API_KEY"
(or zero-key: run it WITHOUT the --header and complete the browser OAuth — it auto-provisions a free key.)
* Cursor / generic MCP config (mcp.json / settings):
{ "mcpServers": { "framefetch": { "url": "https://framefetch.net/mcp", "headers": { "Authorization": "Bearer $FRAMEFETCH_API_KEY" } } } }
MCP tools exposed: framefetch_extract (the workhorse — metadata/transcript/frames/OCR/translation/audio/structured in one call), framefetch_search (find YouTube videos by keyword), framefetch_account (balance + usage), framefetch_platform_capabilities (supported platforms + live health).
- REST (if no MCP). Base https://framefetch.net, header Authorization: Bearer $FRAMEFETCH_API_KEY :
POST /v1/search {"query":"...","limit":10} find YouTube videos by keyword (no url yet? start here)
POST /v1/extract {"url":"...","fields":["metadata","transcript"]} one call, everything you usually need
add frames+OCR with: "fields":["metadata","transcript","frames","text_overlay"], "frames":{"mode":"fps","fps":1,"width":480}
(the "frames" field ALWAYS needs a "frames" spec; the OCR field is text_overlay, with an underscore)
POST /v1/metadata | /v1/transcript | /v1/frames single-purpose variants
POST /v1/batch {"urls":[...],"fields":[...]} many URLs at once
POST /v1/watch {"channel":"","webhook_url":""} POST on each new upload
GET /v1/account balance + usage. Full spec: https://framefetch.net/openapi.json Agent guide: https://framefetch.net/llms.txt
3) VERIFY with one real call and show me the parsed result:
curl -X POST https://framefetch.net/v1/extract -H "Authorization: Bearer $FRAMEFETCH_API_KEY" -H "Content-Type: application/json" -d '{"url":"https://www.youtube.com/watch?v=jNQXAC9IVRw","fields":["metadata","transcript"]}'
Confirm it returned a title + transcript, then tell me it is ready and give me one example of how I would ask you to use it.
Notes: pay-per-call after the free tier (metadata from $0.00015, no subscription); every response itemizes its exact cost; agents can also top up with USDC via x402 (see https://framefetch.net/docs#pay).
Prefer to do it yourself? The rest of this page is the full reference — start at Authenticate.
1 · Authenticate
In a browser: framefetch.net/signup — email in, key out, 100 free calls/month applied, and the key stays re-viewable in your account. Or by API (no human approval):
curl -X POST https://framefetch.net/v1/keys \
-H "Content-Type: application/json" \
-d '{ "email": "you@example.com" }'
# -> { "key": "ff_xxx_yyy", "message": "store this key; it is shown once..." }Send it as Authorization: Bearer ff_xxx_yyy on every call.
2 · Extract
POST /v1/extract — one call, choose your fields.
| Body field | Type | Meaning |
|---|---|---|
url | string (required) | The video URL (supported hosts only) |
fields | string[] | Any of metadata, insights, transcript, frames, text_overlay, digest, audio_digest, structured, comments, comment_sentiment, delta. Default ["metadata"] |
frames | object | Frame spec (see below). Required if frames or text_overlay is in fields |
verbosity | string | Accepted (concise/detailed) but not yet wired into the response — every field's output is identical either way. Reserved for future use. |
voice | string | Spoken voice for audio_digest — see Audio briefings. Default "alloy" |
comments_cap | integer | Top-level comments to fetch for comments, 1–200 — see Comments & audience sentiment. Default 100 |
ask | string | A question about the video (3–500 chars); adds a short answer with timestamped quotes — see Ask |
cache | boolean | Default true. Set false to skip the cache and force a fresh extraction — billed at the real cost of the work done, not the cache-hit floor. The fresh result is written back to the cache as usual, so the next normal call still benefits. |
curl -X POST https://framefetch.net/v1/extract \
-H "Authorization: Bearer ff_xxx_yyy" \
-H "Content-Type: application/json" \
-d '{
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"fields": ["metadata", "transcript", "frames", "text_overlay"],
"frames": { "mode": "fps", "fps": 1, "width": 480 }
}'Identical requests are normally served from FrameFetch's content-hash cache at the $0.002 price floor — the fastest, cheapest path. Pass "cache": false when you specifically need a fresh pull (monitoring a video that keeps changing, or retrying after a degraded/stale result): it skips the cache read for that one call only and is billed like any other fresh extraction, never the floor. It never forks the cache — a cache:false call reads and writes the exact same entry a normal call for the same url/fields would.
3 · Scoped shortcuts
Same auth and billing, with preset fields — for agents that want just one thing.
| Endpoint | Returns |
|---|---|
POST /v1/metadata | metadata + insights |
POST /v1/transcript | transcript only |
POST /v1/frames | frames only (needs a frames spec) |
curl -X POST https://framefetch.net/v1/transcript \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{ "url": "https://www.tiktok.com/@user/video/123" }'3b · Search
POST /v1/search — don't have a URL yet? Find YouTube videos by keyword (via yt-dlp's search — no login, no per-video extraction, no download). YouTube only today; call /v1/extract on a hit's url for the full data.
| Body field | Type | Meaning |
|---|---|---|
query | string (required) | Search keywords, 2–200 chars |
limit | integer | Max results, 1–25. Default 10 |
platform | string | Only "youtube" is supported — any other value is a 400. Default "youtube" |
cache | boolean | Same as /v1/extract's cache — default true |
curl -X POST https://framefetch.net/v1/search \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{ "query": "origami tutorial", "limit": 5 }'
# -> { "platform": "youtube", "query": "origami tutorial",
# "results": [{ "url": "https://www.youtube.com/watch?v=...", "title": "...",
# "uploader": "...", "durationSec": 1284, "uploadDate": null,
# "thumbnail": "https://i.ytimg.com/...", "views": 36084 }, ...],
# "cost": { "totalMicros": 2000 } }Each result carries only what yt-dlp's lightweight flat search actually returns — nothing invented. In practice uploadDate is almost always null (yt-dlp's search rarely reports it); the other fields are reliably populated. Billed a flat, floor-pinned $0.002 per call regardless of limit — same price class as a bare metadata-only extract — including a genuine zero-result search (an empty results array is a normal 200, not an error). Identical query+limit+platform calls hit the same content-hash cache /v1/extract uses. On MCP, this is the framefetch_search tool — see MCP below. See the YouTube Video Search API page for the search-then-ask pairing.
4 · Frames spec
| Field | Values |
|---|---|
mode | all · every_n (+n) · fps (+fps, ≤60) · range (+from,to,fps) |
format | jpg · png · webp |
width | 16–7680 px (downscale — lower = cheaper) |
Up to 1000 frames per call. Frames return as time-limited signed image URLs.
5 · Text overlay (OCR) new
Add "text_overlay" to fields to run OCR on each extracted frame — burned-in captions, price tags, signage. Requires frames to also be requested, with a spec. Priced per frame, same as frames.
"textOverlay": [ { "index": 12, "text": "INSPIRATIONAL HOLIDAY VIDEO", "lines": [ { "text": "INSPIRATIONAL", "confidence": 0.96, "bbox": [74, 144, 702, 63] }, { "text": "HOLIDAY VIDEO", "confidence": 0.96, "bbox": [73, 226, 710, 64] } ] } ]
One entry per frame, same index as the matching frames[] item. A frame with no detected text returns text: "", lines: [] — not an error. Requests estimated at over 200 frames skip text_overlay with a warning rather than running a very long OCR job; narrow the frames spec (shorter range or lower fps) to stay under that.
6 · Batch — many URLs at once new
POST /v1/batch — send up to 10 URLs and get one result per URL, in input order. Each URL is validated and processed on its own: a bad or failing URL comes back as { "url", "ok": false, "error" } without failing the rest. Billed per successful item at that item's own cost; failed items are not billed. Auth, balance, and rate limit are checked once for the whole batch. Frames/OCR need a per-URL frames spec, so request those one at a time via /v1/extract.
curl -X POST https://framefetch.net/v1/batch \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{
"urls": [
"https://www.youtube.com/watch?v=jNQXAC9IVRw",
"https://www.tiktok.com/@user/video/123"
],
"fields": ["metadata", "insights"]
}'{
"results": [
{ "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw", "ok": true, "platform": "youtube", "metadata": { "title": "Me at the zoo" }, "cost": { "totalMicros": 2000 } },
{ "url": "https://www.tiktok.com/@user/video/123", "ok": false, "error": { "code": "EXTRACTION_FAILED", "message": "…" } }
],
"cost": { "totalMicros": 2000 }
}Up to 10 URLs per call. fields applies to every URL. The top-level cost is the sum of the successful items.
7 · Digest — LLM summary new
Add "digest" to fields for a compact summary instead of parsing the whole transcript yourself. FrameFetch runs the transcript through an LLM and returns a short gist, a few topics, and an optional state object of structured facts it was confident about. Requesting digest automatically includes transcript as the source it was derived from; if no transcript is available the field is simply omitted. Priced at $0.00045 per digest, on top of the transcript.
"digest": { "gist": "A first-person clip from the San Diego Zoo; the narrator points out the elephants and their very long trunks.", "topics": ["zoo", "elephants", "vlog"], "state": { "language": "en", "sentiment": "positive" } }
Best-effort: on a model or parse failure the whole digest field is dropped rather than returned half-built.
8 · Video delta — track change over time new
Add "delta" to fields to compare this fetch against the most-recent previous fetch of the same URL. You get engagement velocity — views, likes, and comments per hour — plus flags for whether the creator changed the title or thumbnail. The first time a URL is seen there is nothing to compare against, so delta is just { "hasPrior": false }. A per-hour rate is null when a counter was unavailable or no time had elapsed. Delta responses are never served from cache.
"delta": { "hasPrior": true, "sinceTs": 1751500000000, "viewsPerHr": 812.5, "likesPerHr": 40.2, "commentsPerHr": 3.1, "titleChanged": false, "thumbnailChanged": true }
The video's poster image is also returned as metadata.thumbnail, so a thumbnail swap is easy to diff yourself.
9 · Markdown output new
Prefer Markdown over JSON? Add ?format=md (or ?format=markdown) to any extract-family call — /v1/extract, /v1/metadata, /v1/transcript, /v1/frames — and the same result comes back as clean Markdown with Content-Type: text/plain. An Accept: text/plain or Accept: text/markdown header does the same thing. It is opt-in: JSON stays the default, and an ambiguous Accept (such as the one axios sends by default) always gets JSON.
curl -X POST "https://framefetch.net/v1/extract?format=md" \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{ "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw", "fields": ["metadata", "transcript"] }'# Me at the zoo - **Platform:** youtube - **URL:** https://www.youtube.com/watch?v=jNQXAC9IVRw - **Uploader:** jawed - **Duration:** 19s - **Captions available:** yes ## Transcript All right, so here we are in front of the elephants…
10 · Channel monitor — webhook on every new video new
Subscribe to a creator/channel and FrameFetch POSTs a webhook to you for each new upload — no polling on your side. Manage subscriptions with POST, GET and DELETE /v1/watch. Any valid key works and subscription management is not billed per call. Up to 50 active subscriptions per account.
curl -X POST https://framefetch.net/v1/watch \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{
"channel": "https://www.youtube.com/@creator",
"webhook_url": "https://your-app.example.com/hooks/framefetch"
}'
# -> { "subscription": { "id": "…", "platform": "youtube", "channel": "…",
# "webhook_url": "…", "last_seen_video_id": null, "active": true, "created_at": "…" } }FrameFetch polls each channel on a schedule and, for every new video, POSTs this payload to your webhook_url (best-effort fresh metadata attached; delivery retries a few times on failure):
{
"event": "new_video",
"url": "https://www.youtube.com/watch?v=…",
"metadata": { "title": "…", "uploader": "…", "durationSec": 42 }
}| Call | Does |
|---|---|
POST /v1/watch | Subscribe — body { channel, webhook_url } |
GET /v1/watch | List your active subscriptions |
DELETE /v1/watch/:id | Unsubscribe |
channel must be a creator/channel URL on a supported platform. webhook_url is SSRF-hardened at creation and re-checked before every delivery — it must be a public https URL (localhost, loopback, private/RFC1918, link-local and metadata addresses are rejected). Subscribing never re-notifies you for the channel's existing backlog; you only get videos posted after you subscribe.
11 · Public video pages (/v/<id>) new
Add "publish": true to any extract-family call (/v1/extract, /v1/metadata, /v1/transcript, /v1/frames) and FrameFetch mints a public, shareable SEO page for that video at https://framefetch.net/v/<id>. It is opt-in and privacy-conscious: the page carries only public metadata (title, uploader, duration, view count, platform thumbnail) plus a short, fair-use transcript excerpt — never the full transcript and never a signed frame URL. Each page ships VideoObject structured data and is listed in /sitemap-videos.xml.
curl -X POST https://framefetch.net/v1/extract \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{ "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw", "fields": ["metadata","transcript"], "publish": true }'The /v/<id> id is a stable hash of the video URL, so re-publishing the same video updates the same page. Takedown / creator opt-out: email [email protected] and the page returns a 404.
12 · Response shape
{
"platform": "youtube",
"url": "https://www.youtube.com/watch?v=...",
"metadata": { "title": "...", "uploader": "...", "durationSec": 19, "uploadDate": "...", "thumbnail": "https://.../poster.jpg" },
"insights": { "views": 1234, "likes": 56, "commentCount": 7 },
"transcript": { "text": "...", "source": "whisper" },
"digest": { "gist": "...", "topics": ["..."], "state": {} },
"audio_digest": { "url": "https://.../audio_digest_....mp3?X-Amz-...", "seconds": 24, "voice": "alloy", "provider": "openai" },
"ask": { "answer": "...", "quotes": [ { "t_sec": 4, "text": "..." } ], "confidence": "high", "based_on": ["transcript"] },
"delta": { "hasPrior": true, "viewsPerHr": 812.5, "titleChanged": false, "thumbnailChanged": true },
"frames": [ { "index": 1, "url": "https://.../frame_00001.jpg", "tSec": 0 } ],
"textOverlay": [ { "index": 1, "text": "", "lines": [] } ],
"cost": { "totalMicros": 2000, "breakdownMicros": { "metadata": 150, "transcript": 0, "frames": 0, "textOverlay": 0, "proxy": 0 } }
}Only requested fields are present. cost is always included (micro-USD; 1,000,000 = $1).
13 · Errors
All errors are JSON: { "error": { "code", "message", "hint" } }. Codes are stable so agents can self-correct.
| HTTP | code | Meaning |
|---|---|---|
| 400 | INVALID_PARAMS · INVALID_URL · UNSUPPORTED_HOST · OVER_LIMIT | Bad request / unsupported video / over a cap. These carry an extra error.details object with what was expected vs. what arrived, plus a runnable example_body. |
| 400 | WRONG_ENDPOINT | A JSON-RPC / MCP body (e.g. an initialize handshake) was POSTed to a REST endpoint. The MCP endpoint is https://framefetch.net/mcp — point your MCP client there, not at /v1/extract. |
| 401 | UNAUTHORIZED | Missing/invalid key |
| 402 | PAYMENT_REQUIRED | Out of credit — top up and retry |
| 404 | NOT_FOUND | No such subscription / resource (e.g. DELETE /v1/watch/:id) |
| 405 | METHOD_NOT_ALLOWED | Wrong HTTP method for the route (see the Allow header) |
| 409 | SUBTITLES_UNAVAILABLE | ?format=srt|vtt requested but no timed transcript segments to build subtitles from (see Translation & subtitles) |
| 429 | RATE_LIMITED | Slow down (see Retry-After) |
| 500 | EXTRACTION_FAILED · INTERNAL | Server-side; safe to retry |
| 502 | SEARCH_FAILED | /v1/search's yt-dlp call failed (every attempt threw); a genuine zero-result search is a normal 200, never this |
14 · MCP (for agents)
Streamable HTTP MCP server at POST https://framefetch.net/mcp (send your Bearer key). Tools:
| Tool | Purpose |
|---|---|
framefetch_extract | Extract metadata/transcript/frames/text_overlay from a URL; optional translate + subtitle_format (see Translation & subtitles), voice for a spoken audio_digest briefing (see Audio briefings), and ask for a short answer + timestamped quotes (see Ask) |
framefetch_search | Find YouTube videos by keyword — no URL yet? Start here (see Search) |
framefetch_platform_capabilities | What each platform supports |
framefetch_account | Your balance + recent usage (read-only, never charged) |
Example MCP client config (Claude Desktop / Cursor style):
{
"mcpServers": {
"framefetch": {
"url": "https://framefetch.net/mcp",
"headers": { "Authorization": "Bearer ff_xxx_yyy" }
}
}
}https://framefetch.net/mcp speaks JSON-RPC 2.0 and is the only address an MCP client should ever be pointed at.
The /v1/* endpoints are plain JSON and are not interchangeable with it: sending an MCP initialize handshake to /v1/extract answers
400 WRONG_ENDPOINT and tells you this. If your MCP client reports a 400 about a missing url, you have the wrong URL configured.
14b · Go
REST from Go — plain net/http, no SDK needed:
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]any{
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"fields": []string{"metadata", "transcript"},
})
req, _ := http.NewRequest("POST", "https://framefetch.net/v1/extract", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("FRAMEFETCH_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var out struct {
Metadata struct{ Title, Uploader string } `json:"metadata"`
Transcript struct{ Text string } `json:"transcript"`
Error struct{ Code, Message string } `json:"error"`
}
json.NewDecoder(resp.Body).Decode(&out)
if resp.StatusCode != 200 {
fmt.Println("error:", out.Error.Code, "-", out.Error.Message)
return
}
fmt.Println(out.Metadata.Title, "by", out.Metadata.Uploader)
fmt.Println(out.Transcript.Text)
}MCP from Go — point the client at /mcp, never at /v1/extract (that is a REST path and will answer 400 WRONG_ENDPOINT):
// go get github.com/modelcontextprotocol/go-sdk/mcp
transport := &mcp.StreamableClientTransport{
Endpoint: "https://framefetch.net/mcp", // <-- the MCP endpoint, not /v1/extract
HTTPClient: &http.Client{Transport: authHeader{key: os.Getenv("FRAMEFETCH_API_KEY")}},
}
client := mcp.NewClient(&mcp.Implementation{Name: "my-agent", Version: "1.0.0"}, nil)
session, err := client.Connect(ctx, transport, nil)
// then: session.CallTool(ctx, &mcp.CallToolParams{Name: "framefetch_extract", Arguments: ...})No key yet? POST /v1/keys returns one instantly (see Authenticate) — no card, no verification.
15 · Paying with x402 (no account)
POST /v1/topup without payment returns 402 with x402 requirements (USDC on Base). Pay and retry with the X-PAYMENT header to add 1 USDC = $1 credit. Designed for autonomous agents — no signup, no human.
16 · Platforms
YouTube · YouTube Shorts · TikTok · Instagram Reels · Pinterest · Reddit. Live matrix at GET /v1/platforms — snapshot below. Per-platform guides: YouTube · TikTok · Instagram · Reddit · Pinterest.
| Platform | Metadata & insights | Transcript | Frames & on-screen text (OCR) | Comments | Channel monitor |
|---|---|---|---|---|---|
| YouTube (incl. Shorts) | ✓ | ✓ captions, or Whisper fallback | ✓ | ✓ | ✓ |
| TikTok | ✓ | ✓ Whisper | ✓ | ✗ | ✓ |
| Instagram Reels | ✓ | ✓ Whisper | ✓ | ✗ | ✓ |
| ✓ | ✗ pins are silent/music-only | ✓ | ✗ | ✓ | |
| ✓ | ✓ Whisper | ✓ | ✗ deprecated by Reddit 2026-05-28 | ✓ |
17 · Account & usage new
Two read-only endpoints — same Bearer-key auth as the extract family, but neither one ever bills. GET /v1/account keeps working even at zero balance, so you can always check what's going on.
| Endpoint | Returns |
|---|---|
GET /v1/account | Email, signup date, balance_micros, spend_today_micros, spend_month_micros, current limits, and your auto_topup config |
GET /v1/usage?from&to&group=day|endpoint | Usage history bucketed by calendar day or by endpoint, plus a total |
curl https://framefetch.net/v1/account -H "Authorization: Bearer ff_xxx_yyy"
{
"email": "you@example.com",
"created_at": "2026-01-04T10:02:00.000Z",
"balance_micros": 4850000,
"spend_today_micros": 12000,
"spend_month_micros": 341000,
"limits": { "maxFrames": 1000, "maxBatchUrls": 10, "maxWatchSubscriptions": 50 },
"auto_topup": { "enabled": false, "threshold_micros": null, "amount_micros": null }
}GET /v1/usage defaults to the trailing 30 days (range capped at 366 days) and group=day:
curl "https://framefetch.net/v1/usage?from=2026-06-01&to=2026-07-01&group=endpoint" \ -H "Authorization: Bearer ff_xxx_yyy" # -> { "items": [ { "bucket": "watch:notification", "calls": 340, "cost_micros": 34000 }, ... ], # "total_micros": 125000 }
On MCP, the read-only framefetch_account tool (no input) is this same balance/usage check — the MCP counterpart of GET /v1/account. See MCP below.
18 · Billing — checkout, packs, portal, auto-topup new
POST /v1/checkout now takes an optional pack — bigger packs carry a bonus:
pack | Charge | Credit | Bonus |
|---|---|---|---|
"5" (default) | $5 | 5,000,000 µ | — |
"20" | $20 | 21,000,000 µ | +5% |
"100" | $100 | 112,000,000 µ | +12% |
curl -X POST https://framefetch.net/v1/checkout \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{ "pack": "20" }'
# -> { "url": "https://checkout.stripe.com/…" }Checkout also collects a VAT/Tax ID for B2B buyers and always issues an invoice, and it saves the card for future use by auto-topup.
| Endpoint | Does |
|---|---|
POST /v1/billing/portal | Returns a Stripe Billing Portal link to update your saved card or download invoices. 409 if you haven't checked out yet. |
POST /v1/billing/auto-topup | Opt in/out of balance-floor auto-recharge — body { enabled, threshold_micros, amount_micros } |
curl -X POST https://framefetch.net/v1/billing/auto-topup \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{ "enabled": true, "threshold_micros": 500000, "amount_micros": 5000000 }'Enabling requires a saved card from a prior /v1/checkout (409 PAYMENT_REQUIRED otherwise) and a threshold of at least $0.01. amount_micros must match one of the pack credit amounts above. At most one recharge fires per drop below the threshold; a declined card disables auto-topup and emails you rather than retrying forever. Paid credits never expire — no clock, no forced use-it-or-lose-it.
19 · Translation & subtitles new
Add a top-level "translate": "<iso-639-1>" to /v1/extract or /v1/transcript to also translate the transcript — 25 languages supported (en, es, de, fr, pt, it, nl, pl, tr, ru, uk, ar, hi, id, vi, th, ja, ko, zh, sv, da, no, fi, cs, ro). Requires "transcript" in fields (or "digest", which pulls it in). Adds transcript_translated alongside the untouched original transcript — existing clients that never pass translate see no change at all. Billed per audio-minute like transcription, $0.0015/audio-minute, even when the source transcript came from free captions, since translation is a separate LLM pass.
curl -X POST https://framefetch.net/v1/extract \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"fields": ["transcript"],
"translate": "es"
}'"transcript": { "text": "All right, so here we are...", "source": "captions" }, "transcript_translated": { "lang": "es", "text": "Muy bien, aquí estamos...", "segments": [ { "start": 0, "end": 3.2, "text": "Muy bien, aquí estamos..." } ] }
Prefer a ready-to-save subtitle file over JSON? Add ?format=srt or ?format=vtt to any extract-family call (/v1/extract, /v1/metadata, /v1/transcript, /v1/frames) and get back SubRip or WebVTT instead — built from the transcript's timed segments, using the translated ones automatically when translate is also set. Purely an egress view: same cached result, same cost, same cache key as the JSON call. Requires "transcript" in fields; a result with no timed segments (e.g. an older cache entry) returns 409 SUBTITLES_UNAVAILABLE. Subtitles are included at no extra cost beyond the transcript (and translation, if requested).
curl -X POST "https://framefetch.net/v1/extract?format=srt" \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{ "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw", "fields": ["transcript"], "translate": "es" }'1 00:00:00,000 --> 00:00:03,200 Muy bien, aquí estamos...
On MCP, framefetch_extract takes the same translate arg plus subtitle_format: "srt"|"vtt", which adds a top-level subtitles string to the JSON tool result (MCP results are always JSON, so it can't switch content type like the HTTP ?format= query param does). Useful for EU Accessibility Act (enforced since June 2025) caption requirements: one call gets a translated, ready-to-publish subtitle file for any supported social-video URL.
20 · Audio briefings new
Add "audio_digest" to fields to get a spoken mp3 briefing of the video instead of just reading its transcript or digest. It auto-includes "digest" (which in turn auto-includes "transcript") and synthesizes the digest's gist to speech, returning audio_digest: { url, seconds, voice, provider, lang? } — url is a time-limited (24h) signed link to the mp3, seconds is an estimate of the spoken duration (also the billing basis), and lang is present only when a translate target drove a target-language digest (see below). Pick the voice with a top-level "voice" param: the six OpenAI gpt-4o-mini-tts voices (alloy default, echo, fable, onyx, nova, shimmer — multilingual) or Groq/PlayAI's "Fritz-PlayAI" (English/Arabic-only fallback, used when no OpenAI key is configured). Billed at $0.000375 per estimated spoken second (~3-4¢ for a 90s briefing) on top of the digest/transcript it's derived from — and only when the mp3 is actually produced.
curl -X POST https://framefetch.net/v1/extract \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"fields": ["audio_digest"]
}'"audio_digest": { "url": "https://.../audio_digest_....mp3?X-Amz-...", "seconds": 24, "voice": "alloy", "provider": "openai" }
Combine with translate for a briefing spoken in another language — the digest itself is generated directly in that target language in the same call, so a YouTube link goes in and a Spanish spoken briefing comes out:
curl -X POST https://framefetch.net/v1/extract \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"fields": ["audio_digest"],
"translate": "es",
"voice": "nova"
}'"digest": { "gist": "Un vídeo en primera persona desde el zoológico de San Diego...", "topics": ["zoológico", "elefantes"] }, "audio_digest": { "url": "https://.../audio_digest_....mp3?X-Amz-...", "seconds": 24, "voice": "nova", "provider": "openai", "lang": "es" }
Best-effort, like digest and translate: if no TTS provider is configured (no OPENAI_API_KEY/GROQ_API_KEY) or the only configured provider can't speak the requested language (Groq/PlayAI is English/Arabic-only — set an OpenAI key for other languages), audio_digest is simply omitted, a warning is appended to the top-level warnings array, and the call is not charged for the audio — the rest of the response is unaffected. These degrade cases never surface as an HTTP error code. On MCP, framefetch_extract takes the same fields: ["audio_digest"], translate, and voice arguments.
21 · Structured video understanding new
Add "structured" to fields for a typed, structured reading of the video itself — not just its transcript. It auto-includes "transcript" and also triggers the engine's own keyframe pass over the source video (so it works even on caption-less sources), then runs one gpt-4o-mini vision call (temperature 0) over the transcript plus up to 12 sampled keyframes. The response carries structured: { chapters, entities, products_shown, claims, key_moments, keyframe_mode? } — chapters are timespan segments (start_sec/end_sec/title/summary); entities are people/orgs/places/products/other seen or heard; products_shown notes whether a brand was "visual", "spoken", or "both"; claims are factual statements, optionally timestamped; key_moments flag hooks/reveals/demo steps; keyframe_mode records whether frames came from content-aware scene-cut detection or an even-sampling fallback. Deterministic and cacheable (unlike audio_digest, it carries no expiring signed URL). Billed a flat $0.03 per video — charged only when the analysis is actually produced.
curl -X POST https://framefetch.net/v1/extract \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"fields": ["structured"]
}'"structured": { "chapters": [ { "start_sec": 0, "end_sec": 19, "title": "At the zoo", "summary": "A first-person walk past the elephant enclosure." } ], "entities": [ { "name": "San Diego Zoo", "type": "place" }, { "name": "elephant", "type": "other" } ], "products_shown": [], "claims": [ { "text": "Elephants have really, really long trunks.", "timestamp_sec": 14 } ], "key_moments": [ { "timestamp_sec": 3, "description": "The narrator points the camera at the elephants." } ], "keyframe_mode": "uniform" }
Best-effort, like digest: a missing vision provider, a non-2xx from the provider, or unparseable model output all degrade the same way — structured is omitted, a warning is appended to the top-level warnings array, and the call is not charged for it. These never surface as an HTTP error code, only as a warnings[] string. Works in /v1/batch the same as digest. On MCP, framefetch_extract takes the same fields: ["structured"] argument. See the Video Understanding API page for more.
22 · Comments & audience sentiment new
Add "comments" to fields for up to comments_cap top-level comments per video — independent of the transcript (no video download). Supported on YouTube only (via yt-dlp's info-json comments); TikTok/Instagram/Pinterest have no reliable public comment source, and Reddit's public JSON API — supported here until Reddit deprecated unauthenticated access to it on 2026-05-28 — now returns a hard 403 on every request. All four omit the field with a warning, never a charge (check GET /v1/platforms). Data-minimized by design: author is the display handle only — never a user id, profile URL, or avatar. Priced flat at $0.0045 per call (independent of comments_cap), charged only when comments were actually produced. Add "comment_sentiment" for an aggregated read of the audience mood as a whole — one Groq LLM pass over up to 100 of the fetched comments — which automatically pulls in "comments" as its input. Needs at least 5 fetched comments to run; fewer, or a model failure, omits it with a warning and is never charged. Priced flat at $0.006 per call, on top of the comments call it rides on. No competitor offers this rollup.
curl -X POST https://framefetch.net/v1/extract \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"fields": ["comments", "comment_sentiment"],
"comments_cap": 50
}'"comments": { "items": [ { "text": "This made my day!", "author": "@zoofan", "like_count": 42, "reply_count": 2 } ], "total_fetched": 50, "cap_applied": 50, "sort": "top" }, "comment_sentiment": { "positive_pct": 78, "neutral_pct": 18, "negative_pct": 4, "summary": "Viewers overwhelmingly find the clip charming and nostalgic, with a few noting the low video quality.", "top_themes": ["nostalgia", "elephants", "video quality"], "representative": { "positive": "This made my day!" } }
Best-effort, like digest/structured: a fetch/parse failure, or an unsupported platform (COMMENTS_UNSUPPORTED), omits comments (and thus comment_sentiment) with a warning appended to the top-level warnings array — never an HTTP error, and never charged. Works in /v1/batch too, though batch always uses the default comments_cap of 100 (batch does not accept a top-level comments_cap). On MCP, framefetch_extract takes the same fields: ["comments", "comment_sentiment"] plus a top-level comments_cap argument.
23 · Ask — question answering new
Add a top-level "ask": "<question>" (3–500 chars) to /v1/extract or /v1/batch for a short, direct answer instead of parsing the whole transcript yourself: one Groq LLM pass reads the transcript — with timestamps when available — and returns ask: { answer, quotes, confidence, based_on }. quotes are excerpts quoted verbatim from the transcript, each with a t_sec (the supporting segment's start time, or null) — grounded evidence instead of a 25k-token dump. Requesting ask automatically includes "transcript" as its input; it deliberately does not auto-include "structured" (too expensive to imply), but if you separately request "structured" in the same call, its chapters feed the answer too (based_on then reads ["transcript","chapters"]). The answer comes back in whatever language the question was asked in. Priced flat at $0.0075 per call, charged only when an answer is actually produced. Never served from the result cache — a repeat call with a different question always gets a genuinely fresh answer, even though the underlying transcript/metadata it draws on still cache normally for every other call.
curl -X POST https://framefetch.net/v1/extract \
-H "Authorization: Bearer ff_xxx_yyy" -H "Content-Type: application/json" \
-d '{
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"ask": "What animal does the narrator point out?"
}'"ask": { "answer": "The narrator points out the elephants and their very long trunks.", "quotes": [ { "t_sec": 4, "text": "they have really really really long trunks" } ], "confidence": "high", "based_on": ["transcript"] }
Grounded, not guessed: every quote is post-validated against the actual transcript text before it's ever returned — a quote the model couldn't back up with real transcript text is dropped, and if every quote drops the answer is still returned but confidence is forced to "low". Best-effort like digest: no transcript AND no frames fallback available (see below), or a model/parse failure, omits ask with a warning appended to the top-level warnings array and the call is not charged for it — never an HTTP error code. Works in /v1/batch: every item gets its own fresh answer to the same question. On MCP, framefetch_extract takes the same top-level ask argument.
No transcript? Falls back to frames, not a decline. A platform with no caption/transcript source at all (Pinterest — GET /v1/platforms reports transcript: false) or any video whose transcription failed still answers ask, grounded in the same sampled keyframe images the structured field analyzes, via a separate vision model call. That answer honestly reports the weaker evidence: based_on reads exactly ["frames"], quotes is always [] (there is no transcript text to quote verbatim), confidence can never be "high", and coverage.mode reads "frames". Same flat $0.0075 price either way — no surcharge for the frames path. This does not change transcript: false on the capability matrix: Pinterest genuinely still has no transcript, ask just no longer needs one.
24 · FAQ
What happens if one requested field fails but the others succeed?
The call still returns 200 with every field that DID succeed. The failed field is simply omitted (or, for some fields, returned with partial data) and a warnings array on the response explains what happened and why — a single field failing does not fail the whole extraction.
Is caching different for ask, audio_digest, or delta?
Yes. delta and audio_digest always run fresh and are never served from cache — delta must re-read the prior snapshot on every call, and an audio_digest's signed mp3 URL expires and its voice varies per request. ask is read-through: the underlying extraction (transcript, metadata, etc.) can be served from cache, but the answer itself is always computed fresh for your specific question and is never cached or reused for a different question.
What happens if I request more than 1000 frames?
1000 is a hard cap on frames per call, enforced at the ffmpeg extraction step itself (not just billing) — a spec that would yield more is truncated to the first 1000 frames and the response includes a warning explaining why, instead of failing the whole call. Narrow frames.range, raise frames.every_n's n, or lower fps to get every frame you actually want.
Is there an official client library or SDK?
No dedicated SDK yet — FrameFetch is a plain REST + JSON API (any HTTP client works) plus a Streamable HTTP MCP server for AI agents. See the MCP section for ready-to-paste client config.
Can I use FrameFetch without creating an account?
Yes — autonomous agents can pay per top-up with x402 (USDC on Base) and never create an account or provide an email. See the x402 section.