---
title: "The long-form hooks and packages you kept"
method: GET
path: "/v1/finds/keeps"
tags: ["long form", "packaging"]
---

# The long-form hooks and packages you kept

`GET /v1/finds/keeps`

## Request

- Base URL: `https://api.hookdetector.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.hookdetector.com/v1/finds/keeps`
- Auth: none declared

The videos you kept, newest decision first: one entry a video on each tab (its
newest find), or the kept finds of one run when run_id is given; full find objects, a
page at a time. count is the number on this page. Free. MCP: list_kept_finds.

## Query parameters

- `kind` string, nullable: only long or only packaging; leave out for both
- `run_id` string, uuid, nullable
- `limit` integer: page size, 1 to 100
- `cursor` string, nullable: next_cursor from the previous page; leave out for the first

## Headers

- `authorization` string, nullable: Bearer hd_... from POST /v1/accounts or POST /v1/keys

## Response `200`

Successful Response

- KeptFindsOut
  - `kept` FindOut[], required
    - `find_id` string, uuid: the id to keep, reject or read it by; absent on an item of `progress.items`
    - `run_id` string, uuid: the run that found it
    - `kind` 'long' | 'packaging': long or packaging
    - `rank` integer: its place in the run, from 1: the strongest outlier first
    - `verdict` 'keep' | 'reject' | 'null', nullable: keep, reject or null
    - `video_id` string: the YouTube video id
    - `platform` string: always "youtube"
    - `title` string: the video's title as YouTube shows it, word for word (runs of spaces read as one)
    - `channel` string, nullable: the channel's name
    - `channel_handle` string, nullable: the channel's @handle, without the @
    - `watch_url` string: where to watch it
    - `player_url` string: an iframe src that plays it (16:9)
    - `thumbnail_url` string: the video's thumbnail on YouTube's image host; `thumbnail_urls` lists every size, best first, and a size a video lacks answers 404
    - `views` integer: views when the run read it
    - `likes` integer, nullable: likes, when YouTube gave them
    - `comments` integer, nullable: comments, when YouTube gave them
    - `age_days` number, nullable: days since it was posted, when the run read it
    - `views_per_day` number, nullable: views divided by age_days
    - `baseline_videos` integer, nullable: how many recent uploads the channel's usual was measured on
    - `found_by` string, nullable: the search that surfaced it
    - `duration_s` integer, nullable: length in seconds; always over 180
    - `posted_at` string, nullable: when it was posted (ISO 8601). Exact when `posted_exact` is true; otherwise the latest it can be, from YouTube's "3 months ago", and never before 2023 either way
    - `posted_exact` boolean: whether posted_at is the real timestamp
    - `channel_median_views` integer, nullable: the channel's usual: the median views of its recent long-form uploads
    - `outlier` number, nullable: views divided by channel_median_views: 28.2 means 28 times what this channel normally gets
    - `outlier_score` integer: the video on one fixed scale, 0 to 100: the outlier, its reach, how fresh it is and how hard its viewers reacted, placed between two fixed anchors. Nothing else a run found moves it, so finds from different runs rank together; it moves only with the video's own numbers and age
    - `channel_size` 'small' | 'medium' | 'large' | 'null', nullable: the channel by what its uploads usually get (channel_median_views): small, medium or large
    - `seen_before` boolean: true when an earlier run of yours on this tab already found this video; a keep or reject made then is already on it
    - `analysis_from_earlier_read` boolean: true when the opening or the package analysis is the one an earlier run of yours wrote for this video; the numbers are this run's
    - `channel_id` string, nullable: the channel's YouTube id (UC...)
    - `thumbnail_urls` string[]: every thumbnail size, best first; a size a video lacks answers 404
    - `score` number, nullable: the ranking score outlier_score is scaled from
    - `hook_lines` integer: long: how many lines of `intro` the hook reaches, counted from line 1; the last may be only partly the hook, `hook_words` counts exactly
    - `hook_end_s` number: long: the second the hook ends, to a tenth, never null. Inside a line it is estimated at the opening's own pace
    - `hook_start_s` number: long: the second the hook starts: 0 or close to it, later when a sponsor read or a greeting came first
    - `hook_words` integer: long: the hook's length in words
    - `auto_captions` boolean: long: true when the opening carries almost no punctuation (auto captions): its sentences are not marked and names may be misheard
    - `about_subject` 'yes' | 'partly' | 'null', nullable: long: whether the video is about the run's subject, read from its title and opening in the call that analyses it: yes, or partly (the subject is one part of it, or it covers a neighbouring subject). Null when the question was not asked (a channel or video link) or not answered. A video the model answers "no" for is never delivered
    - `hook_type_other` string, nullable: long: the device in words when hook_type is "other"
    - `title_device_other` string, nullable: packaging: the device in words when title_device is "other"
    - `outlier_tier` 'outlier' | 'strong' | 'breakout' | 'null', nullable: outlier (under 5 times the channel's usual), strong (5 to 10) or breakout (10 or more)
    - `likes_per_1k` number, nullable: likes for every 1,000 views
    - `comments_per_1k` number, nullable: comments for every 1,000 views
    - `style_match` boolean, nullable: when the request asked for a kind of opening or title: whether this one is that kind. Null when it asked for none
    - `angle` string, nullable: the specific angle the video takes on the subject
    - `format` string, nullable: long: the kind of video, one of talking_head, voiceover, vlog, tutorial, interview, documentary, challenge, review, story, other
    - `opens_with` string, nullable: long: how the first seconds begin, one of cold_open, greeting, teaser, question, sponsor, other
    - `thumbnail_checked` boolean: packaging: true when thumbnail_url is the size YouTube really served and the analysis looked at
    - `thumbnail_sha256` string: packaging: a digest of the picture the analysis looked at; a later run reuses the analysis only while YouTube serves the same picture
    - `analysed_at` string: packaging: when the thumbnail was looked at (ISO 8601); a reused analysis keeps its own date
    - `title_chars` integer: packaging: the title's length in characters
    - `cut_on_phone` boolean: packaging: true when the title is longer than 60 characters, where a phone roughly cuts a title. Approximate: YouTube publishes no limit, and the cut moves with the screen, the font and the letters (about 58 to 62)
    - `thumbnail_text_role` string, nullable: packaging: how the words on the thumbnail stand to the title, one of repeats_title, adds_to_title, contrasts_title, no_text. Counted in code from `title_overlap` where that can be (repeats_title at 0.7 or more, adds_to_title under 0.3); between, the model's reading
    - `title_overlap` number, nullable: packaging: the share of the thumbnail's words that are also in the title, 0 to 1, counted in code. null when the thumbnail has no words, or its words are in a script the title does not use
    - `on_subject` 'yes' | 'partly' | 'no' | 'null', nullable: packaging: whether the look, told the run's subject, judged the package to be about it: yes, or partly (it covers the subject as one part of something broader). A package judged no is never delivered. null when the run was on a channel's or a video's address, when the look gave no usable verdict, or when the analysis is one an earlier run wrote (its subject may have been another)
    - `thumbnail_faces` integer, nullable: packaging: how many faces the thumbnail shows
    - `thumbnail_colors` string, nullable: packaging: the colours that carry the frame
    - `why` string, nullable: one line naming the outlier in words
    - `language` string, nullable: the language spoken, as YouTube names the caption track
    - `analysed` boolean: false when the model could not analyse it: the find is still real, its analysis fields are null
    - `hook` string: long: the hook, verbatim: the lines of `intro` the analysis marks as the hook, cut at the last sentence end that keeps it inside 25 seconds and 60 words, a leading sponsor read or greeting left out. The whole opening is `intro_text`
    - `hook_seconds` number, nullable: long: the second the hook ends; the same number as `hook_end_s`, kept for older readers
    - `intro` object[]: long: the opening as it was said, cut by the transcript's own clock: lines of n (from 1), t (start second, null when the transcript had no clock) and text
    - `intro_text` string: long: the same words as one string
    - `intro_seconds` integer: long: how much of the opening was read
    - `hook_type` string, nullable: long: since 2026-10-02: the device the opening uses, one of curiosity_gap, contrarian_claim, specific_number, identity_callout, stakes_threat, promise_payoff, pattern_interrupt, story_open, question, direct_command, other (then `hook_type_other` names it in two to five words). Null on older hooks and when the write-up gave none. A client should treat an unknown value as other.
    - `mechanism` string, nullable: long: why the opening holds a viewer, quoting it word for word
    - `promise` string, nullable: what the viewer is told they will get
    - `open_loop` string, nullable: long: the question the opening leaves unanswered
    - `title_link` string, nullable: long: how the first lines pay off the title
    - `beats` object[]: long: the opening in consecutive parts: from and to (line numbers of `intro`), beat (one of hook, stakes, proof, credibility, context, promise, preview, story, payoff, cta, other) and does
    - `template` string, nullable: long: the hook as a fill-in structure with {placeholders}
    - `steal_it` string, nullable: how another niche reuses the move
    - `audience` string, nullable: the viewer it is made for
    - `title_device` string, nullable: packaging: the title's device, one of curiosity_gap, contrarian_claim, specific_number, how_to, listicle, challenge, story, question, warning, comparison, authority, timeliness, identity_callout, other
    - `title_template` string, nullable: packaging: the title as a fill-in structure with {placeholders}
    - `thumbnail_text` string, nullable: packaging: the words printed on the thumbnail, as a vision model read them; null when none
    - `thumbnail_subject` string, nullable: packaging: what the picture shows
    - `thumbnail_emotion` string, nullable: packaging: the feeling it signals
    - `thumbnail_composition` string, nullable: packaging: how the frame is built
    - `thumbnail_devices` string[]: packaging: up to four of face_closeup, big_text, arrow_or_circle, before_after, number, contrast_color, object_hero, screenshot, split_screen, action_shot, collage, minimal, other
    - `gap` string, nullable: packaging: what the thumbnail shows that the title does not say, and the question that leaves
  - `count` integer, required: how many are on this page
  - `next_cursor` string, nullable, required

## Other responses

- `401`: missing, invalid, revoked or expired API key (code key_expired)
- `403`: the key lacks the scope this call needs (insufficient_scope), or its credit_limit is spent (key_credit_limit_reached)
- `413`: request body over the size cap
- `422`: invalid input
- `500`: our fault; names the request id

---

[API](https://skmtc.dev/hookdetector/apis/hook-detector.md) · [All operations](https://skmtc.dev/hookdetector/apis/hook-detector/llms.txt) · [OpenAPI document](https://skmtc.dev/hookdetector/apis/hook-detector/revisions/b136a2804d2b?raw)
