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.
Auth · Extract · Scoped endpoints · 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
Get a key (free $0.05 credit, shown once). No human approval.
curl -X POST https://framefetch.net/v1/keys \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]" }'
# -> { "key": "ff_xxx_yyy", "message": "store this key; it is shown once..." }Send it as Authorization: Bearer ff_xxx_yyy on every call.
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 | Optional response detail level |
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 30 |
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 }
}'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" }'| 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.
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.
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.
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.
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.
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…
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.
/v/<id>) newAdd "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.
{
"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" },
"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).
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 |
| 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) |
| 404 | VIDEO_UNAVAILABLE | The platform itself says this video is gone (removed, or it never existed) or private. Deterministic and never charged — no retry, no proxy and no other region can make it appear. Fix the URL. |
| 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 |
| 503 | SERVER_BUSY | The extraction queue is full right now. Back off and retry (see Retry-After). Never charged. |
| 504 | PROCESSING_TIMEOUT | The job was slower than our response deadline. Not an error in your request, and never charged — see the retry contract below. |
The 504 retry contract. A heavy job (long video, many frames, several fields at once) can outrun the deadline we answer within. When that happens we return a real JSON 504 PROCESSING_TIMEOUT instead of letting the connection die — and the job keeps running on our side. When it finishes, its result is written to the cache. So the recovery is simply to re-send the identical request (same URL, same fields): it lands as a cache hit and is billed at the price floor. The 504 itself costs nothing.
The instructions ride on the response, so an agent needs no prior knowledge of any of this — retryable, retry_after_sec and a plain-language retry sentence sit next to error (never inside it, so existing parsers are unaffected), and retry_after_sec is mirrored in the Retry-After header. Wait the suggested time: retrying sooner can miss the cache and start a second, fully-priced run.
{
"error": {
"code": "PROCESSING_TIMEOUT",
"message": "processing exceeded framefetch's own response deadline",
"hint": "…retry the identical request in ~60s to collect it at the price floor…"
},
"retryable": true,
"retry_after_sec": 60,
"retry": "the job is still running server-side and its finished result will be cached — retry the IDENTICAL request (same url + fields) after ~60s to collect it as a cache hit at the price floor."
}On POST /v1/batch the same contract applies per item: a slow url comes back as { "url": …, "ok": false, "error": { "code": "PROCESSING_TIMEOUT" }, "retryable": true, "retry_after_sec": 60 } while the rest of the batch is unaffected. A batch also has one shared budget, so a url the batch never got around to starting is returned with retry_after_sec: 0 — nothing ran for it and nothing was charged, so you can re-send it right away (ideally in a smaller batch).
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) and voice for a spoken audio_digest briefing (see Audio briefings) |
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" }
}
}
}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.
YouTube · YouTube Shorts · TikTok · Instagram Reels · Pinterest · Reddit. Capability matrix: GET /v1/platforms. Per-platform guides: YouTube · TikTok · Instagram · Reddit · Pinterest.
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": "[email protected]",
"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.
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.
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.
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.
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.
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). Reddit's own public comment source worked the same way until Reddit deprecated unauthenticated access to it on 2026-05-28 — it now returns a hard 403 — so Reddit degrades exactly like TikTok/Instagram/Pinterest, which never had a reliable public comment source; 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 30 (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.