API and MCP
Everything the app does, your agent can do. MCP and REST run on one backend and return the same hook.
https://api.hookdetector.comSections
Quick start
Three calls, no card. New accounts need a beta access code: Ask for a code.
- Get a key.
The key comes back once, with 500 free credits. Store it.
curl -X POST https://api.hookdetector.com/v1/accounts -H 'content-type: application/json' -d '{"access_code": "HD-XXXX-XXXX"}' - Start a run.
A fresh run takes two to three minutes.
curl -X POST https://api.hookdetector.com/v1/research \ -H "authorization: Bearer hd_YOUR_KEY" \ -H "content-type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"topic":"short term rental tax strategy","count":10}' - Wait for it.
Repeat until
statusisdoneorfailed. Set your client's read timeout above the wait, for example 70 seconds.curl "https://api.hookdetector.com/v1/runs/$RUN_ID?wait=55" -H "authorization: Bearer hd_YOUR_KEY"
Signed in already? Your keys are on your account page.
Connect your agent
The MCP server is https://api.hookdetector.com/mcp over streamable HTTP. Replace hd_YOUR_KEY with your key, or reveal it above and copy the block as it is.
One command in your terminal. Checked against the live server on 2026-09-22.
claude mcp add --transport http hookdetector https://api.hookdetector.com/mcp \ --header "Authorization: Bearer hd_YOUR_KEY"
URL-only connectors and the 30 tools
A connector that only takes a URL: add https://api.hookdetector.com/mcp with no auth, call create_account once, then pass the key it returns as api_key on every tool.
- create_account
- find_hooks
- find_long_hooks
- find_packaging
- get_run
- list_runs
- export_run
- get_hook
- embed_hook
- remix_hook
- keep_hook
- clear_decision
- list_keeps
- list_finds
- get_find
- remix_find
- keep_find
- clear_find_decision
- list_kept_finds
- chat
- list_conversations
- get_conversation
- balance
- usage
- list_keys
- create_key
- revoke_key
- delete_account
- showcase
- request_access
Each tool returns the same shape as its REST route. find_hooks, find_long_hooks, find_packaging and get_run take wait_seconds from 0 to 50. A failed tool call carries {"error": {"code": <status>, "message", "error_code"}}.
Routes
All of them take Authorization: Bearer hd_... except signup.
| Route | What it does |
|---|---|
POST /v1/accounts | Sign up. No auth. Returns a key and 500 free credits. |
GET /v1/me | Your account, balance and the key you called with. |
GET /v1/keys | Your active keys, never the keys themselves. |
POST /v1/keys | Make another key with its own permissions, spend cap and expiry. Shown once. |
DELETE /v1/keys/{key_id} | Revoke a key. The signup key is "original"; "others" revokes every key except the one you call with, for a leak. |
DELETE /v1/account | Erase the account and everything in it. |
| Route | What it does |
|---|---|
POST /v1/research | Start a hook run on TikTok, Instagram and YouTube Shorts. Answers 202 at once. Honours Idempotency-Key. |
GET /v1/runs/{run_id} | One run of any kind and its results. ?wait=0 to 55 holds until it finishes. |
GET /v1/runs | Your runs, newest first. ?kind=short, long or packaging narrows it. |
GET /v1/runs/{run_id}/export | A finished run of any kind as csv, json or md. |
| Route | What it does |
|---|---|
POST /v1/longform | Start a long-form run: how the YouTube outliers on a topic open. The topic may be a channel's or a video's link. 202 at once; results are the run's finds. |
POST /v1/packaging | Start a packaging run: the titles and thumbnails of those outliers, what they share, and written ideas. |
GET /v1/finds | The index: every video your long and packaging runs found, once each, by outlier score. ?q= searches it; ?kind=, ?tier=, ?min_score=, ?device= and ?sort= narrow and order it. |
GET /v1/finds/{find_id} | One result of a long or packaging run, every field. |
POST /v1/finds/{find_id}/remix | Write openings (from a long find) or packages (from a packaging find) for your niche, or with mode intro and a video_brief, your own timed intro in a long find's structure. 1 credit. Always labelled as written. |
POST /v1/finds/{find_id}/decision | Keep or reject a find. |
DELETE /v1/finds/{find_id}/decision | Clear a verdict on a find. |
GET /v1/finds/keeps | The finds you kept. ?kind= and ?run_id= narrow it. |
| Route | What it does |
|---|---|
GET /v1/hooks/{hook_id} | One hook, every field, including player_url. |
GET /v1/hooks/{hook_id}/embed | The platform's official embed markup. |
POST /v1/hooks/{hook_id}/remix | Write lines from a hook's structure for your niche. 1 credit. Always labelled as written. |
| Route | What it does |
|---|---|
POST /v1/hooks/{hook_id}/decision | Keep or reject a hook. |
DELETE /v1/hooks/{hook_id}/decision | Clear a verdict, the way an undo does. |
GET /v1/keeps | The hooks you kept. ?run_id= narrows it to one run. |
| Route | What it does |
|---|---|
POST /v1/chat | One chat turn: an answer, a clarifying question, or a run it started. |
GET /v1/conversations | Your conversations. |
GET /v1/conversations/{conversation_id} | One conversation, its messages and runs. |
| Route | What it does |
|---|---|
GET /v1/usage | Balance, spend, what runs in flight hold, the last 30 days. |
Runs
POST /v1/research answers 202 with a run_id and credits_reserved. Any language or dialect, from any country.
| Field | Accepts | Default |
|---|---|---|
topic | 3 to 500 characters; only its subject is searched | required |
count | 1 to 30 | default 10 |
reuse | true starts from clips you already paid for on the same topic | default true |
conversation_id | a conversation's id | optional |
language | free text up to 60 characters naming the language or dialect every clip must be in, such as "Moroccan Darija", "ar-MA" or "Brazilian Portuguese" | optional |
country | an ISO 3166-1 alpha-2 code for where the creators are, such as MA or BR; any other value is 422 | optional |
Saying the language or country in the topic works too; the fields win when both are given. Runs echo both, and each hook carries the language it was judged to be in. MCP find_hooks takes the same two arguments.
Wait
?wait= from 0 to 55 holds the answer until the run finishes or the time runs out. One account may hold ten waiting requests at once.
Progress
A run in flight carries progress: a stage, a plain message you can show a person, and updated_at. The stage is one of queued, starting, reusing, searching_instagram, searching_tiktok, searching_youtube, judging, transcribing, writing, saving, done, failed, other; treat other as still working. counts and, once a hook is written, preview hold the hooks confirmed so far (line, why, watch_url, player_url, thumb_url). Add ?since= with the last updated_at and a waiting call answers as soon as the progress moves.
"progress": {
"stage": "searching_tiktok",
"message": "searching TikTok, round 1",
"updated_at": "2026-09-22T12:31:40Z"
}Retry safely
Send an Idempotency-Key header, one fresh value per run. The same key on the same account within 24 hours returns the first run with 200 and Idempotent-Replayed: true, and reserves nothing. The same key with a different body is 422 idempotency_key_reused.
Lists and cursors
/v1/runs, /v1/keeps and /v1/conversations take limit and return next_cursor; pass it back as ?cursor=. It is null on the last page.
Long form and packaging
Two more kinds of run, both on long-form YouTube only (never Shorts), both outliers only, nothing posted before 2023. An outlier is a video whose views are at least min_outlier times the median of its own channel's recent uploads. Both answer 202 with a run_id and are read with GET /v1/runs/{run_id}?wait=55. Their results are the run's finds; progress.items holds those written so far. report.landscape counts how many videos the search returned, how many beat their channel, and how many of those came from small channels. A channel's address (https://www.youtube.com/@name or @name) returns that channel's own outliers; a video's (https://www.youtube.com/watch?v=...) returns that one video, whatever its multiple. A Short's address is refused.
| Field | Accepts | Default |
|---|---|---|
topic | a subject, or a YouTube channel's or video's address | required |
min_outlier | 1.5 to 100: views at least this many times the channel's recent median | default 2 |
posted_within_days | 30, 90, 365 or 730 | default since 2023 |
channel_size | small, medium or large, by what the channel's uploads usually get | optional |
language, country | as for a hook run | optional |
reuse | true starts from the videos your last run on this topic bought (within a week); a video already read is not read again | default true |
Long form
| Field | Accepts | Default |
|---|---|---|
count | 1 to 30 | default 8 |
intro_seconds | 20 to 120 | default 60 |
openers | 0 to 6 openings written for your topic | default 3 |
Reads the opening of each video from its own captions. Each find carries hook and intro (timed lines, verbatim), hook_type, mechanism, promise, open_loop, beats, template and steal_it. The hook is a run of words of the opening: hook_start_s, hook_end_s and hook_words say where it runs and how long it is, auto_captions is true when the captions carry almost no punctuation (names in them may be misheard), and about_subject is yes or partly (null on a link run). Beside title, views, channel_median_views, outlier and outlier_score (0 to 100 on one fixed scale, as of the run that found it). report.patterns names the openings several share and why each holds, report.shape counts how they are built, and report.openers holds the written openings, labelled by openers_label, never counted as finds. It reserves up to 90 credits (more for a count above 20, up to 140 at 30) and charges only the provider calls it made.
Packaging
| Field | Accepts | Default |
|---|---|---|
count | 1 to 30 | default 12 |
ideas | 0 to 10 titles and thumbnail concepts written for your topic | default 6 |
Looks at each thumbnail beside its title. Each find carries title (YouTube's own, word for word), thumbnail_url (the image that was looked at), title_device, title_template, thumbnail_text, thumbnail_subject, thumbnail_text_role, thumbnail_composition, thumbnail_devices, gap and outlier_score, and says on_subject (yes or partly), title_overlap (the share of the thumbnail's words that the title repeats) and cut_on_phone (the title is longer than about 60 characters, where a phone roughly cuts it; approximate). report holds title_patterns, thumbnail_patterns, numbers (title length, how many carry a number or a question, how many thumbnails print words) and ideas, labelled by ideas_label, never counted as finds. It reserves up to 70 credits (up to 88 at a count of 30) and charges only the provider calls it made; looking at a thumbnail costs none.
curl -X POST https://api.hookdetector.com/v1/longform \
-H "authorization: Bearer hd_YOUR_KEY" -H "content-type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"topic":"home espresso","count":8,"min_outlier":3,"openers":3}'
curl -X POST https://api.hookdetector.com/v1/packaging \
-H "authorization: Bearer hd_YOUR_KEY" -H "content-type: application/json" \
-d '{"topic":"home espresso","count":12,"ideas":6}'The finds index. Every video your runs found is in one index, once each: GET /v1/finds?kind=packaging&sort=score. q searches titles, channels, hooks, the words on thumbnails and angles; tier (outlier, strong, breakout), min_score and device narrow it; sort is score, outlier, views or newest. Keep or reject with POST /v1/finds/{find_id}/decision; kept ones come back from GET /v1/finds/keeps. A verdict belongs to the video, in every run that found it.
Remix. POST /v1/finds/{find_id}/remix with a niche writes up to n (1 to 5, default 3) openings (from a long find) or packages (from a packaging find) on that find's structure, for 1 credit. They come back in written under a label that says they were written; they are never finds. Send mode intro and a video_brief (your video in a few sentences, 10 to 600 characters) to a long find instead and the answer is your own intro in that opening's structure: each beat with its seconds, a word budget, the words to say with {placeholders} for what only you know, and on-screen and B-roll suggestions. Also 1 credit.
Hooks, keeps and chat
Each hook carries its opening line (hook), its best line, what it argues, why it spread, its topic, the text on screen, the full transcript, its language, views, likes, the creator and a watch_url. A clip with no words has a null hook: we will not invent one from a caption.
To play a clip, put player_url in an iframe sized to aspect (9:16): TikTok's official player or Instagram's embed. TikTok photo posts have a null player_url; read their slides from onscreen_text. The embed route returns the platform's own markup, or kind: "fallback" when the creator turned embedding off.
curl https://api.hookdetector.com/v1/hooks/$HOOK_ID -H "authorization: Bearer hd_YOUR_KEY" curl https://api.hookdetector.com/v1/hooks/$HOOK_ID/embed -H "authorization: Bearer hd_YOUR_KEY"
Keep or reject
curl -X POST https://api.hookdetector.com/v1/hooks/$HOOK_ID/decision \
-H "authorization: Bearer hd_YOUR_KEY" -H "content-type: application/json" \
-d '{"verdict":"keep"}'
curl "https://api.hookdetector.com/v1/keeps?run_id=$RUN_ID" -H "authorization: Bearer hd_YOUR_KEY"The verdict is keep or reject. A DELETE on the same path clears it.
Chat
POST /v1/chat takes message, an optional conversation_id and an optional count. It answers a question about delivered hooks (action: "answer"), asks one clarifying question ("clarify") or starts a run ("research", with the run in run). Turns are limited per account per hour. The Short form tab uses it; an agent usually calls research directly.
Keys and usage
Up to ten active keys per account, so each agent gets its own. A new key is shown once. The account always keeps at least one active key. scopes picks what a key may do; with none it gets read, write and research, never admin. credit_limit (1 to 100,000) caps what it may ever spend, and expires_in_days (1 to 365) ends it. A missing permission is 403 insufficient_scope; past its cap, 403 key_limit_reached; expired, 401 key_expired. If a key leaks, DELETE /v1/keys/others stops every key but the one you call with.
| Scope | Allows |
|---|---|
read | every GET |
write | keep, pass, chat |
research | start runs, which spends credits |
admin | manage keys, delete the account |
Show the calls
curl https://api.hookdetector.com/v1/keys -H "authorization: Bearer hd_YOUR_KEY"
curl -X POST https://api.hookdetector.com/v1/keys -H "authorization: Bearer hd_YOUR_KEY" \
-H "content-type: application/json" \
-d '{"name":"claude-code on laptop","scopes":["research","read","write"],"credit_limit":500,"expires_in_days":90}'
curl -X DELETE https://api.hookdetector.com/v1/keys/$KEY_ID -H "authorization: Bearer hd_YOUR_KEY"
curl -X DELETE https://api.hookdetector.com/v1/keys/others -H "authorization: Bearer hd_YOUR_KEY"Credits
One credit is one call to the data provider. A run holds up to 200, charges only what it used and gives the rest back. A failed run of any kind costs nothing, and so does a run that finds nothing (a few a day per account); past that it is charged what it used, and its outcome says why it came back short. Asking again about a topic you already paid for reuses those clips, so it costs far less.
curl https://api.hookdetector.com/v1/usage -H "authorization: Bearer hd_YOUR_KEY"
It returns credits, spent_total, reserved_now, runs_total, last_30_days and your 20 most recent runs.
Export
A finished run as csv (the default), json or md. Cells a spreadsheet would run as a formula are neutralised. A run still working is 409 run_not_finished. Over MCP it is export_run. Add &bom=true for a csv a person opens in Excel, so curly quotes and emoji read right.
curl -o hooks.csv "https://api.hookdetector.com/v1/runs/$RUN_ID/export?format=csv&bom=true" -H "authorization: Bearer hd_YOUR_KEY"
Delete your account
Erases the account and every row that belongs to it: all its keys, runs, hooks, decisions, conversations and messages. Every key stops working at once. It needs the exact confirmation below, and it is refused with 409 run_in_flight while a run is still working. Over MCP it is delete_account with the same confirm. In the app: Account, then More.
curl -X DELETE https://api.hookdetector.com/v1/account -H "authorization: Bearer hd_YOUR_KEY" \
-H "content-type: application/json" \
-d '{"confirm":"delete my account"}'Errors and limits
Every error is JSON: {"detail": "...", "code": "..."}. Show detail to a person; branch on code, and on the HTTP status for a code you do not know. Every response carries X-Request-Id; send your own to trace a call.
| Status | Codes | Meaning |
|---|---|---|
| 401 | invalid_api_key, key_expired | The key is missing, unknown, revoked or past its expiry. |
| 402 | insufficient_credits | Not enough credits for a run. The detail states your balance. |
| 403 | insufficient_scope, key_limit_reached | The key may not do that (detail names the permission it lacks), or it reached its spend cap. |
| 404 | run_not_found, hook_not_found, find_not_found, conversation_not_found, key_not_found, not_found | Nothing with that id on this account, or no such route. |
| 405 | method_not_allowed | The route exists; that method does not. |
| 409 | key_limit_reached, last_active_key, run_in_flight, run_not_finished | The request conflicts with the account's state: ten keys already, the last key, a run still working. |
| 413 | payload_too_large | The body is over 64 KB. |
| 422 | invalid_request, invalid_cursor, idempotency_key_reused, confirmation_required, remix_not_possible | Invalid input: detail names the field; errors lists every problem. remix_not_possible: every line the model wrote broke a rule, or it declined; nothing was charged, try again or another niche. |
| 429 | rate_limited, too_many_runs | A limit was hit, or the account already has the most research runs in flight. Wait Retry-After seconds. |
| 500 | internal_error | Our fault. detail names the request id. Nothing was charged. |
| 503 | model_unavailable, unavailable, search_unavailable, at_capacity, restarting | The language model, the video search provider or the service cannot answer, today's research capacity is used up, or the server is restarting. Nothing was charged; wait Retry-After seconds. |
Rate limits
Signups are limited per address, and chat turns and new keys per account, each per hour. Those responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the oldest counted request leaves the hour). A 429 carries Retry-After in seconds. Waiting requests are capped at ten per account.
Agent files: llms.txt · docs.md · openapi.json · interactive reference · mcp.json · server.json · status.