Concepts
Research
How an intent becomes a digest, why a research id is a run id, and how to re-slice what you collected for free instead of paying for it twice.
A research answers a question you would otherwise answer by watching a hundred videos: what is working in this niche right now, who is doing it, and what they open with. You describe the goal in plain language, and a digest comes back with the evidence still attached. It is the most expensive thing this API does, and everything you collected is then readable for nothing, forever — which is the part most people miss.
Three things to know first
A research id is a run id. There is no separate research resource. POST /v1/research answers with a run, and that run’s id is what every read below takes. The same id works on GET /v1/runs/{id}, which is how you poll one that is still going. If you are holding an id and wondering which of the two it is, it is both.
You pay to collect, then read free forever. The $0.50 buys the collection: the searches, the analysis and the digest. Asking a narrower question afterwards — TikTok only, over a hundred thousand views, best outlier first — is a free read of what you already have, not a second research. If you find yourself running a second one to narrow the first, you are paying $0.50 for something the reads do for nothing.
Reads are frozen by default. Every number a read serves is the one the research itself saw, so the evidence can never contradict the digest computed from it. Passing as_of=latest serves the newest figures instead — useful a month later, when the question is which of these kept growing — and every list read, and the digest, says which basis it served.
What a research does
- Derives keywords from the intent. One model call turns “why are cold plunge videos taking off” into at most 4 search terms. Supplying
keywordsyourself skips that call entirely: if you already know what to search for, you should not pay a model to be told. If the derivation fails, the run searches the intent verbatim rather than failing — a worse search, not a lost $0.50. - Searches each requested platform for every keyword — all of them by default — and de-duplicates by platform id, so the same video found twice is one candidate. The request narrows the search with
platforms(any of tiktok, instagram, youtube),region, andwindow, which ismonthunless you say otherwise; the reference has every value. - Ranks the candidates and analyses the top
max_videos— 50 by default, 200 at most. Which videos make that cut is the single decision that most affects what the digest can say. - Scores them against their creators’ baselines where a baseline exists, so “500k views” becomes “six times what this creator usually does”. See Scores.
- Writes one digest over the result.
That is minutes of work, so a research is always async: the call waits up to your wait budget, returns the finished run if it lands inside that window, and otherwise hands back a run to poll. See Runs for the envelope and the polling pattern.
POST /v1/research·$0.50 covering up to 50 freshly enriched videos, then $0.01 each, so at most $2.00 at `max_videos` 200. Videos whose analysis is already stored are free; failed videos are never charged. The 402 pre-check uses that worst case.
curl -X POST "https://www.shortsintel.com/v1/research" \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"intent":"why are cold plunge videos taking off","platforms":["tiktok"],"window":"month","max_videos":50}'const response = await fetch(
"https://www.shortsintel.com/v1/research",
{
method: "POST",
headers: {
"Authorization": "Bearer sk_live_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"intent": "why are cold plunge videos taking off",
"platforms": [
"tiktok"
],
"window": "month",
"max_videos": 50
}),
},
);
if (!response.ok) {
throw new Error(`ShortsIntel ${response.status}`);
}
const data = await response.json();import requests
response = requests.post(
"https://www.shortsintel.com/v1/research",
headers={
"Authorization": "Bearer sk_live_YOUR_KEY",
"Content-Type": "application/json",
},
json={
"intent": "why are cold plunge videos taking off",
"platforms": [
"tiktok",
],
"window": "month",
"max_videos": 50,
},
)
response.raise_for_status()
data = response.json()200 · The Run finished: `{ run, result }`. Research is always async, so in practice this arrives from `GET /v1/runs/{id}`. The example is a `partial` Run: two of the fifty videos could not be fetched, and neither was charged for.
{
"run": {
"id": "run_8f2c1ad04b",
"kind": "research",
"status": "partial",
"created_at": "2026-09-08T09:14:02.418Z",
"completed_at": "2026-09-08T09:14:04.930Z",
"charge_usd": 0.5,
"list_price_usd": 0.5,
"cached": false
},
"result": {
"digest_version": 1,
"intent": "why are cold plunge videos taking off",
"keywords": [
"cold plunge",
"ice bath",
"cold exposure",
"cold plunge routine"
],
"keyword_rationale": "The intent is about a practice rather than a product, so the keywords cover the practice and its two common synonyms.",
"coverage": {
"tiktok": "complete",
"instagram": "partial"
},
"digest": {
"summary": "Cold plunge videos are travelling on measured personal diaries rather than on science explainers. The videos that break out open on an unimpressive first number and pay it off with a later one, and keep the discomfort on camera instead of cutting around it. Sentiment is broadly positive, with the negative share concentrated on the cost of equipment.",
"themes": [
{
"name": "30-day diaries",
"share": 0.45,
"description": "One creator, one tub, a counter on screen, and the change stated as a number rather than a feeling.",
"video_ids": [
"tiktok:7300000000000000000"
]
},
{
"name": "Before-workout plunging",
"share": 0.2,
"description": "Shorter clips arguing about timing relative to training, usually with a gym setting.",
"video_ids": [
"tiktok:7300000000000000042"
]
}
],
"hooks": [
{
"text": "Day one I lasted eleven seconds.",
"video_id": "tiktok:7300000000000000000",
"hook_type": "stat",
"why_it_works": "An unimpressive concrete number sets up a payoff the viewer stays for."
}
],
"sentiment": {
"positive": 0.62,
"neutral": 0.23,
"negative": 0.15,
"note": "Enthusiasm for the routine itself; the negative share is almost entirely the price of a tub."
},
"angles": [
{
"angle": "Open on the day-30 number and work backwards to day one.",
"rationale": "Every diary in the shortlist that led with the outcome outperformed the ones that led with the method."
}
]
},
"videos": [
{
"platform": "tiktok",
"id": "7300000000000000000",
"url": "https://www.tiktok.com/@someone/video/7300000000000000000",
"description": "3 minutes at 4°C every morning for 30 days. Here is what actually changed. #coldplunge #recovery",
"posted_at": "2026-09-05T06:32:11.000Z",
"duration_seconds": 41,
"thumbnail_url": "https://p16-sign.tiktokcdn.com/obj/tos-cover-7300000000000000000.jpeg",
"hashtags": [
"coldplunge",
"recovery",
"morningroutine"
],
"sound": {
"id": "7180000000000000000",
"title": "original sound - someone"
},
"creator": {
"id": "cre_9d4b2a7f1c",
"handle": "someone",
"name": "Someone",
"follower_count": 184300
},
"snapshot": {
"at": "2026-09-08T09:14:04.219Z",
"views": 1284000,
"likes": 143200,
"comments": 2810,
"shares": 9040,
"saves": 21600
},
"scores": {
"outlier": 4.21,
"outlier_p90": 1.6,
"percentile": 95,
"velocity": {
"views_per_hour": 6698.6,
"window": "snapshot"
},
"reason": null,
"baseline": {
"median_views": 305000,
"p90_views": 802000,
"engagement_rate": 0.1342,
"posts_per_week": 4.1,
"sample": 20,
"confidence": "high",
"source": "videos",
"computed_at": "2026-09-08T09:14:03.902Z",
"stale": false
}
}
},
{
"platform": "tiktok",
"id": "7300000000000000042",
"url": "https://www.tiktok.com/@anotherone/video/7300000000000000042",
"description": "I tried cold plunging before every workout for a week. #coldplunge",
"posted_at": "2026-09-06T13:04:52.000Z",
"duration_seconds": 28,
"thumbnail_url": "https://p16-sign.tiktokcdn.com/obj/tos-cover-7300000000000000042.jpeg",
"hashtags": [
"coldplunge",
"gym"
],
"sound": {
"id": "7180000000000000099",
"title": "Winter Air"
},
"creator": {
"id": "cre_1a77c0b3de",
"handle": "anotherone",
"name": "Another One",
"follower_count": 21400
},
"snapshot": {
"at": "2026-09-08T09:14:04.219Z",
"views": 96200,
"likes": 8140,
"comments": 212,
"shares": 401,
"saves": 1180
},
"scores": null
}
],
"creators": [
{
"platform": "tiktok",
"id": "cre_9d4b2a7f1c",
"handle": "someone",
"name": "Someone",
"follower_count": 184300,
"video_count": 3,
"best_outlier": 4.21,
"median_views": 305000,
"total_views": 1611400,
"video_ids": [
"tiktok:7300000000000000000"
]
}
],
"hashtags": [
{
"value": "coldplunge",
"platforms": [
"instagram",
"tiktok"
],
"video_count": 34,
"total_views": 18402000,
"video_ids": [
"tiktok:7300000000000000000",
"instagram:C9xExampleShortcode"
]
}
],
"sounds": [
{
"value": "7180000000000000000",
"title": "original sound - someone",
"platforms": [
"tiktok"
],
"video_count": 4,
"total_views": 2140000,
"video_ids": [
"tiktok:7300000000000000000"
]
}
],
"research": {
"requested": 50,
"completed": 48,
"failed": 2,
"cached": 11,
"fresh": 37,
"max_videos": 50,
"included_videos": 50
}
}
}POST /v1/research · $0.50 covering up to 50 freshly enriched videos, then $0.01 each, so at most $2.00 at `max_videos` 200. Videos whose analysis is already stored are free; failed videos are never charged. The 402 pre-check uses that worst case.
What the digest contains
A summary, the themes with the share of videos behind each, the hooks quoted verbatim rather than paraphrased, the sentiment distribution, and recommended angles. Themes and hooks carry the video ids they came from, so every claim in the digest points back at evidence you can read.
Hooks being verbatim is deliberate: a paraphrased opening line is useless for the thing people want hooks for, which is seeing exactly how a successful video starts.
The price, and what moves it
$0.50 covers up to 50 freshly analysed videos; each one past that is $0.01. So the default research is exactly the $0.50 you were quoted, and raising max_videos to the 200 cap is the one thing that moves the bill.
Two things move it back down. A video whose analysis is already stored is free and uses none of the allowance, so researching a niche twice is cheaper the second time. A video that fails is never charged. A run that analysed some of its shortlist and produced a digest anyway is partial and charges for what it got; a run that could produce no digest at all is failed and charges nothing.
estimate=true quotes the ceiling before you spend anything. It has to be a ceiling: the videos that would decide the real price are the results of searches that have not run yet. See Pricing and estimates.
Coverage, and why Instagram is always partial
Each platform reports its own coverage, and Instagram is reported as partial however well the search went. Its index is best-effort, so a digest drawing on it must not claim completeness. A platform whose searches all failed says so too, rather than quietly contributing nothing. See Platforms and coverage.
Reading it back
GET /v1/research/{id} is the overview: the intent, the keywords derived from it and why, the coverage each platform reported, the counts and the digest. It carries no item lists — those page separately, below.
GET /v1/research/{id} · Free, forever. Collection is the only thing charged.
The run envelope — what POST /v1/research and GET /v1/runs/{id} return — carries the first 25 of each evidence list inline, which is a glance. The four list reads are how you get past that cap, filter it and order it:
GET /v1/research/{id}/videos— The ranked shortlist. Filter by views, platform, posted date, creator, hashtag or sound; order by rank, outlier, views or posted date.GET /v1/research/{id}/creators— The outlier creators behind it, narrowable by follower count and platform, ordered by outlier, baseline median, followers, video count or views.GET /v1/research/{id}/hashtags— The hashtags that carried, narrowable by platform, ordered by how many of the shortlist used them or by the views behind them.GET /v1/research/{id}/sounds— The same, for sounds.
GET /v1/research/{id}/videos·Free, forever, filtered or not. Collection is the only thing charged.
curl "https://www.shortsintel.com/v1/research/id_example/videos?platforms=tiktok%2Cyoutube&start_date=2026-01-01&end_date=2026-01-31&order_by=rank&sort=desc&limit=25&offset=0&as_of=run" \
-H "Authorization: Bearer sk_live_YOUR_KEY"const response = await fetch(
"https://www.shortsintel.com/v1/research/id_example/videos?platforms=tiktok%2Cyoutube&start_date=2026-01-01&end_date=2026-01-31&order_by=rank&sort=desc&limit=25&offset=0&as_of=run",
{
method: "GET",
headers: {
"Authorization": "Bearer sk_live_YOUR_KEY",
},
},
);
if (!response.ok) {
throw new Error(`ShortsIntel ${response.status}`);
}
const data = await response.json();import requests
response = requests.get(
"https://www.shortsintel.com/v1/research/id_example/videos?platforms=tiktok%2Cyoutube&start_date=2026-01-01&end_date=2026-01-31&order_by=rank&sort=desc&limit=25&offset=0&as_of=run",
headers={
"Authorization": "Bearer sk_live_YOUR_KEY",
},
)
response.raise_for_status()
data = response.json()200 · `{ items, total, limit, offset, as_of }`.
{
"items": [
{
"platform": "tiktok",
"id": "7300000000000000000",
"url": "https://www.tiktok.com/@someone/video/7300000000000000000",
"description": "3 minutes at 4°C every morning for 30 days. Here is what actually changed. #coldplunge #recovery",
"posted_at": "2026-09-05T06:32:11.000Z",
"duration_seconds": 41,
"thumbnail_url": "https://p16-sign.tiktokcdn.com/obj/tos-cover-7300000000000000000.jpeg",
"hashtags": [
"coldplunge",
"recovery",
"morningroutine"
],
"sound": {
"id": "7180000000000000000",
"title": "original sound - someone"
},
"creator": {
"id": "cre_9d4b2a7f1c",
"handle": "someone",
"name": "Someone",
"follower_count": 184300
},
"snapshot": {
"at": "2026-09-08T09:14:04.219Z",
"views": 1284000,
"likes": 143200,
"comments": 2810,
"shares": 9040,
"saves": 21600
},
"scores": {
"outlier": 4.21,
"outlier_p90": 1.6,
"percentile": 95,
"velocity": {
"views_per_hour": 6698.6,
"window": "snapshot"
},
"reason": null,
"baseline": {
"median_views": 305000,
"p90_views": 802000,
"engagement_rate": 0.1342,
"posts_per_week": 4.1,
"sample": 20,
"confidence": "high",
"source": "videos",
"computed_at": "2026-09-08T09:14:03.902Z",
"stale": false
}
},
"rank": 0,
"item_id": "ri_5c30f18ab4",
"cached": true
},
{
"platform": "tiktok",
"id": "7300000000000000042",
"url": "https://www.tiktok.com/@anotherone/video/7300000000000000042",
"description": "I tried cold plunging before every workout for a week. #coldplunge",
"posted_at": "2026-09-06T13:04:52.000Z",
"duration_seconds": 28,
"thumbnail_url": "https://p16-sign.tiktokcdn.com/obj/tos-cover-7300000000000000042.jpeg",
"hashtags": [
"coldplunge",
"gym"
],
"sound": {
"id": "7180000000000000099",
"title": "Winter Air"
},
"creator": {
"id": "cre_1a77c0b3de",
"handle": "anotherone",
"name": "Another One",
"follower_count": 21400
},
"snapshot": {
"at": "2026-09-08T09:14:04.219Z",
"views": 96200,
"likes": 8140,
"comments": 212,
"shares": 401,
"saves": 1180
},
"scores": null,
"rank": 1,
"item_id": "ri_5c30f18ab5",
"cached": false
}
],
"total": 48,
"limit": 25,
"offset": 0,
"as_of": "run"
}GET /v1/research/{id}/videos · Free, forever, filtered or not. Collection is the only thing charged.
All four page with limit (default 25, maximum 100) and offset, and report a total. That is deliberately not the cursor the rest of this API pages with: a research is a frozen set of at most 200 videos and the rollups drawn from them, and you choose the ordering, so a count is cheap and more useful than a cursor. An offset past the end is an empty page with the true total, not an error. See Pagination for the cursor everything else uses.
Under as_of=run, the first page of creators, hashtags or sounds at the default ordering is exactly the 25 the run envelope carried inline, and offset=25 continues from there.
GET /v1/research/{id}/digest is the fifth read and the odd one out: the synthesis on its own, with none of the evidence, for feeding into a prompt. It neither pages nor filters.
GET /v1/research/{id}/digest · Free, forever. Collection is the only thing charged.
A research that has not finished has nothing to serve and answers run_not_ready (409) on these reads — the request is fine, the resource is not ready. Note the overview does not: it hands back the run to poll, because an agent that just started a research and immediately reads it back should get “running”, not an error.
A research that failed never becomes readable. The sub-reads keep answering run_not_ready (409) and the overview answers with the failed run’s problem body, exactly as polling does. There is nothing to wait for: start a new research.
Frozen, latest, and refreshing
as_of=run, the default, serves the snapshot the research itself saw. as_of=latest serves the newest this API holds for each video, and the filters and ordering then run over those numbers — min_views under latest means “over that many views now”.
Be aware that latest is uneven until you refresh. A video only gains a fresh figure when something looks at it, so a popular video someone else looked up yesterday is current while the tail of your shortlist still carries what your research recorded. Videos with nothing newer keep the research’s numbers rather than coming back empty, which is why run is the default and why every list read states its basis.
POST /v1/research/{id}/refresh makes it even: it sweeps the videos the research delivered — the ones in its result; videos dropped for balance or that failed are not quoted, swept or charged — at $0.002 each, and charges only for the ones that come back. Today only TikTok videos can be refreshed; videos on other platforms keep the numbers the research recorded, and a research with nothing refreshable is refused up front, with no run and no charge. It is a run rather than a parameter on the reads because it fetches every video again, and a read must never charge. Afterwards as_of=latest reflects the sweep and as_of=run is exactly what it was.
POST /v1/research/{id}/refresh · $0.002 per video that comes back.
The digest is never recomputed by a refresh, and GET /v1/research/{id}/digest always reports as_of=run: a digest is what a model wrote about the shortlist as it stood, not a number joined to a snapshot.
From an assistant
Over MCP the same pair exists and the distinction is the same one: research collects and charges, get_research re-reads what was collected for nothing, taking a run id and an aspect. Its filters must be ones that aspect takes: a filter the aspect does not take is refused, not ignored. See the MCP quickstart.