---
title: "No-signup metadata demo (rate-limited, no key)"
method: POST
path: "/v1/demo"
---

# No-signup metadata demo (rate-limited, no key)

`POST /v1/demo`

## Request

- Base URL: `https://framefetch.net`
- URL: `https://framefetch.net/v1/demo`
- Auth: none required

## Request body

- UrlBody
  - `url` string, uri, required: Supported hosts only: youtube.com, youtu.be, tiktok.com, instagram.com, pinterest.*, reddit.com, redd.it

## Response `200`

Metadata + insights (demo)

- ExtractResult
  - `platform` string
  - `url` string
  - `metadata` object
    - `title` string, nullable
    - `uploader` string, nullable
    - `durationSec` number, nullable
    - `uploadDate` string, nullable: normalized YYYY-MM-DD
    - `sourceFps` number, nullable
    - `thumbnail` string, nullable: poster/cover image URL (additive; may be null or omitted for older callers).
  - `insights` object
  - `transcript` object
    - `text` string
    - `source` string
    - `lang` string: Best-effort source language code, when known.
    - `segments` TranscriptSegment[]: Timed transcript segments (Whisper verbose_json or caption cue timings). Additive: omitted entirely (never []) when the source carries no usable timings.
      - `start` number, required
      - `end` number, required
      - `text` string, required
  - `transcript_translated` object: Present only when the caller passed the `translate` param. An LLM translation of the transcript into the requested ISO-639-1 language, billed per audio-minute like transcription.
    - `lang` string: The requested ISO-639-1 target language.
    - `text` string
    - `segments` TranscriptSegment[]: Mirrors the source cue timings (start/end unchanged, text translated); omitted when the source transcript carried no timings — never back-filled from the untranslated transcript.
      - `start` number, required
      - `end` number, required
      - `text` string, required
  - `digest` object: LLM summary of the transcript (present only when the digest field was requested and a transcript was available).
    - `gist` string
    - `topics` string[]
    - `state` object
  - `audio_digest` AudioDigest: A spoken-audio rendering of the video's digest (present only when "audio_digest" was requested and a digest was available to synthesize). Never served from the content-hash cache — its signed URL expires and the voice can vary per request. Billed per estimated spoken second (from the digest text length, ~15 chars/s) ONLY when the mp3 is actually produced; a TTS/upload failure omits this field, adds a warning to the top-level warnings array, and is not charged — the rest of the extraction stays usable. Failure reasons include no TTS provider configured (analogous to the worker-internal TTS_UNAVAILABLE code: neither OPENAI_API_KEY nor GROQ_API_KEY set) and an unsupported language on the only configured provider (TTS_LANG_UNSUPPORTED: Groq/PlayAI only speaks English/Arabic — set an OpenAI key for other languages); neither surfaces as an HTTP error code, only as a warnings[] string.
    - `url` string: Signed URL to the mp3 (24h TTL).
    - `seconds` number: Estimated spoken duration (digest gist chars / 15), the billing basis — not measured from the mp3.
    - `voice` string: The TTS voice actually used (after per-provider fallback/mapping).
    - `provider` 'openai' | 'groq'
    - `lang` string: ISO-639-1 language the audio is spoken in; present only when a translate target drove a target-language digest.
  - `structured` VideoStructured: Typed, structured understanding of a video (present only when "structured" was requested): the transcript plus up to 12 sampled keyframes are run through a vision LLM (gpt-4o-mini, temperature 0) that returns chapters, entities, products shown, claims, and key moments as strict JSON. Deterministic and cacheable — unlike audio_digest it carries no expiring signed URL, so it takes part in the normal content-hash cache. Best-effort like digest: any download/VLM/parse failure omits the whole field, adds a warning to the top-level warnings array, and is NOT charged; the rest of the extraction stays usable. Worker-internal failure codes (never surfaced as an HTTP error, only as a warnings[] string): STRUCTURED_UNAVAILABLE (no vision provider configured — OPENAI_API_KEY unset), STRUCTURED_PROVIDER_ERROR (the provider returned a non-2xx response), STRUCTURED_PARSE_ERROR (the model's output could not be parsed/validated into this schema). Billed a flat $0.03 per video (perStructuredVideoMicros, floor-pinned — see /pricing) charged only when the analysis is actually produced.
    - `chapters` object[]: Timespan segments of the video.
      - `start_sec` number
      - `end_sec` number
      - `title` string
      - `summary` string
    - `entities` object[]: People/orgs/places/products/other the model saw or heard.
      - `name` string
      - `type` 'person' | 'org' | 'place' | 'product' | 'other'
    - `products_shown` object[]: Products/brands the video shows and/or talks about.
      - `name` string
      - `evidence` 'visual' | 'spoken' | 'both': How the product was surfaced: only on-screen, only mentioned, or both.
    - `claims` object[]: Factual claims made in the video, optionally anchored to a timestamp.
      - `text` string
      - `timestamp_sec` number
    - `key_moments` object[]: Notable moments (hooks, reveals, demo steps) at a specific second.
      - `timestamp_sec` number
      - `description` string
    - `keyframe_mode` 'scene' | 'uniform': How the analyzed frames were selected: 'scene' = content-aware cut detection, 'uniform' = even-sampling fallback for static/short clips with too few detected cuts.
  - `comments` VideoComments: Up to cap_applied TOP-LEVEL comments for a video, sorted by the platform's "top" ranking (present only when "comments" was requested). Supported on YouTube only (via yt-dlp's info-json comments). Reddit's own public comment source — the unauthenticated thread JSON API — worked the same way until Reddit deprecated unauthenticated access to it on 2026-05-28; TikTok/Instagram/Pinterest never had a reliable public comment source. Billed a flat $0.0045 per call (independent of comments_cap), charged only when comments were actually produced. Best-effort: any fetch/parse failure, or an unsupported platform, omits the whole field with a warning and is NOT charged — never surfaces as an HTTP error code. Worker-internal failure codes behind those warnings (never an HTTP error, only a warnings[] string): COMMENTS_UNSUPPORTED (the platform has no comment source — drop "comments" from fields for it; see GET /v1/platforms), COMMENTS_PROVIDER_ERROR (the yt-dlp info-json fetch or parse failed).
    - `items` VideoComment[]
      - `text` string, required
      - `author` string: Display handle only — no user id / profile URL (data minimization).
      - `like_count` integer
      - `reply_count` integer: Number of direct replies to this top-level comment.
    - `total_fetched` integer: How many comments actually came back (after the cap).
    - `cap_applied` integer: The effective cap that was applied (default 30, hard max 200 via comments_cap).
    - `sort` 'top'
  - `comment_sentiment` CommentSentiment: Aggregated LLM sentiment over a video's comments (present only when "comment_sentiment" was requested, which auto-includes "comments" as its input): ONE Groq chat call (llama-3.3-70b-versatile, temperature 0, JSON mode) reads up to 100 comments and returns the audience mood AS A WHOLE — not per-comment scoring. Requires at least 5 fetched comments; fewer (or a model/parse failure) omits the whole field with a warning and is NOT charged — never surfaces as an HTTP error code. Billed a flat $0.006 per call, on top of the $0.0045 "comments" call it rides on. No competitor offers this rollup.
    - `positive_pct` number: 0-100; positive_pct + neutral_pct + negative_pct === 100 (normalized).
    - `neutral_pct` number
    - `negative_pct` number
    - `summary` string: 2-3 plain English sentences characterizing the overall audience reaction.
    - `top_themes` string[]: Up to 5 short recurring themes across the comments.
    - `representative` object: One exemplar comment per polarity, when found.
      - `positive` string
      - `negative` string
  - `delta` object: Change since the most-recent prior fetch of the same url (present only when the delta field was requested). First time a url is seen -> { hasPrior: false } and nothing else. A per-hour rate is null when a counter was unavailable or no time had elapsed.
    - `hasPrior` boolean
    - `sinceTs` integer: epoch ms of the prior snapshot this delta is measured against
    - `viewsPerHr` number, nullable
    - `likesPerHr` number, nullable
    - `commentsPerHr` number, nullable
    - `titleChanged` boolean
    - `thumbnailChanged` boolean
  - `frames` object[]
    - `index` integer
    - `url` string
    - `tSec` number
  - `textOverlay` object[]: On-screen text per frame (OCR), same index as frames.
    - `index` integer
    - `text` string
    - `lines` object[]
      - `text` string
      - `confidence` number
      - `bbox` number[]
  - `cost` object
    - `totalMicros` integer
    - `breakdownMicros` object

## Other responses

- `429`: Error envelope

---

[API](https://skmtc.dev/framefetch/apis/framefetch-api.md) · [All operations](https://skmtc.dev/framefetch/apis/framefetch-api/llms.txt) · [OpenAPI document](https://skmtc.dev/framefetch/apis/framefetch-api/revisions/5584e8eb9bf5?raw)
