Skip to content

API referenceResearch

Research a niche

POST/v1/research

Derives keywords from an intent, searches every platform, de-duplicates by platform id, analyses the top max_videos, scores them against their creators' baselines, ranks them and produces one Digest. Always async: typically 1-3 minutes for 50 videos, depending on how busy the video analysis service is, longer toward 200; run.progress says how far it has got. The full evidence lists page at GET /v1/runs/{id}/items?type=videos|creators|hashtags|sounds. Facebook is opt-in: name it in platforms and its videos are searched and analysed like the others, at the same price. Instagram and Facebook are reported as coverage: "partial".

Open in Playground(no API Key needed)

Price

1,036 credits covering up to 50 enriched videos, then 20 credits each, whether the analysis is fresh or already stored, so at most 4,036 credits at max_videos 200. Failed videos are never charged. The 402 pre-check uses that worst case.

Supports "estimate": true in the body: the reply is the price in Credits and whether it would be cached, with no Run created and nothing charged.

A cached result costs the same as a fresh one; cached says which you got.

Parameters

NameInTypeRequiredDefaultDescription
Idempotency-KeyheaderstringOptional—1-255 printable ASCII characters. Replaying this key with the same payload returns the original Run (202 while it is still running); a different payload, or a different operation, is a 409 idempotency_conflict.

Request body

application/json

object

  • intentstringrequiredlength 1 to 1000What you want to learn, in plain language. At most 1000 characters.
  • keywordsarray of stringitems at most 4Skip keyword derivation and use these. At most 4, each at most 100 characters.
  • platformsarray of stringitems at least 1Omit for TikTok, Instagram and YouTube. facebook is opt-in: name it to research Facebook videos and Reels too; they are analysed like the others, at the same price.
  • regionstringlength 2 to 5ISO country code.
  • windowstringone of day, week, month, quarter, half_year, all
  • max_videosintegervalue 1 to 200Videos to enrich. Default 50, max 200.
  • waitnumbervalue 0 to 120
  • estimatebooleanWhen a source the call would read is unavailable and the answer is not served from cache, the estimate adds unavailable, the { platform, capability } parts it cannot reach, or answers 503 source_unavailable when it can reach none.

Responses

200 OK

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.

any of 2 shapes

  • Option 1object
    • runobjectrequiredEvery charged operation creates a Run. The envelope is identical whether the result came back inline or was polled.
      • idstringrequired
      • kindstringrequiredWhat was run, e.g. video_lookup.
      • statusstringrequiredone of running, completed, partial, failedpartial keeps the completed items and charges for them, up to what the Balance covers; failed charges nothing.
      • created_atstringrequiredRFC 3339.
      • completed_atstringrequirednullableNull while the Run is still running.
      • charge_creditsintegerrequiredWhat this Run actually cost, in Credits. A stored answer costs the same as a fresh one.
      • list_price_creditsintegerrequiredThe full price of the work asked for, in Credits. Equal to charge_credits unless the Run charged for only part of it.
      • cachedbooleanrequiredTrue when a stored result answered without paying the vendor.
      • progressobjectWhat a running Research has done so far, refreshed as it works: stage (keywords, searching, filtering, analysing, writing_digest) and the counts known by then — keywords, found, kept, dropped_off_topic, and analysed of shortlisted. A count is absent until its stage produced it. Absent once the Run settles, and on every other kind.
        • stagestringrequiredone of keywords, searching, filtering, analysing, writing_digest
        • keywordsintegervalue at least 0
        • searchesintegervalue at least 0
        • foundintegervalue at least 0
        • judgedintegervalue at least 0
        • keptintegervalue at least 0
        • dropped_off_topicintegervalue at least 0
        • dropped_unsureintegervalue at least 0
        • shortlistedintegervalue at least 0
        • analysedintegervalue at least 0
        • updated_atstringrequired
    • resultobjectrequired
      • digest_versionintegerrequired
      • intentstringrequired
      • keywordsarray of stringrequired
      • keyword_rationalestringrequired
      • coveragemap of stringrequired
      • digestobjectrequired
        • summarystringrequiredTwo or three sentences answering the intent directly: what is working on this topic right now, across the videos analysed.
        • themesarray of objectrequireditems 1 to 8
          • namestringrequiredShort label, three or four words.
          • sharenumberrequiredvalue 0 to 1Fraction of the analysed videos that belong to this theme, 0-1.
          • descriptionstringrequiredWhat the videos in this theme have in common.
          • video_idsarray of stringrequiredvideo_id keys (platform:id) of the analysed videos that belong to this theme, exactly as given in the evidence.
        • hooksarray of objectrequireditems at most 12
          • textstringrequiredThe opening line, quoted VERBATIM from the evidence. Never paraphrase, never invent.
          • video_idstringrequiredvideo_id key (platform:id) of the video this hook is quoted from, exactly as given in the evidence.
          • hook_typestringrequiredThe kind of hook, e.g. question, claim, negation, stat.
          • why_it_worksstringrequiredOne sentence.
          • templatestringlength at least 1The verbatim hook with its specifics (product, number, mistake, outcome, audience) replaced by short bracketed slots, every other word kept as spoken, e.g. "Stop [mistake] if you want [outcome]".
          • liftobjectnullableThe lift_tables.hook_type row for the quoted video's analysed hook type, attached in code (never written by the model). Null when that video has no analysed hook type.
            • valuestringrequiredThe analysed hook type, or format + hook type, the row is keyed by.
            • video_countintegerrequired
            • median_viewsnumberrequirednullable
            • median_engagement_ratenumberrequirednullable
            • views_liftnumberrequirednullable
            • engagement_liftnumberrequirednullable
            • low_confidencebooleanrequired
        • sentimentobjectrequired
          • positivenumberrequiredvalue 0 to 1
          • neutralnumberrequiredvalue 0 to 1
          • negativenumberrequiredvalue 0 to 1
          • notestringrequiredOne sentence on what drives the distribution.
        • anglesarray of objectrequireditems 1 to 8
          • anglestringrequiredA content angle worth trying.
          • rationalestringrequiredWhy the evidence supports it, referencing what was seen.
        • formulasarray of objectitems at most 5
          • namestringrequiredShort label, three to five words.
          • recipestringrequiredOne line saying how to make it: the format, how it opens, and the order of its beats.
          • visual_formatstringrequiredThe visual format, copied exactly as it appears in lift_tables.visual_format.
          • hook_typestringrequiredThe hook type, copied exactly as it appears in lift_tables.hook_type.
          • structurestringrequiredThe beat structure the example videos share, as short beats joined by arrows, e.g. "problem → demo → result → CTA".
          • video_idsarray of stringrequiredUp to 3 video_id keys (platform:id) of analysed videos that follow this formula, best performing first, exactly as given in the evidence.
          • liftobjectrequirednullableThe lift_tables.format_hook row for this formula's visual format and hook type, attached in code (never written by the model): its video_count and lifts. The lift is for the format + hook type pair only; the structure is not bucketed. Null when no analysed video has that pair.
            • valuestringrequiredThe analysed hook type, or format + hook type, the row is keyed by.
            • video_countintegerrequired
            • median_viewsnumberrequirednullable
            • median_engagement_ratenumberrequirednullable
            • views_liftnumberrequirednullable
            • engagement_liftnumberrequirednullable
            • low_confidencebooleanrequired
        • gapsarray of objectitems at most 5Up to 5 things the audience asks for in the top 10 videos' comments that no analysed video addresses, each with verbatim quoted comments (and the video they came from) and a suggested angle. Empty when no comments were available. Absent on research recorded before gaps existed.
          • askstringrequiredWhat the audience asks for or complains about, in one sentence, in your own words.
          • commentsarray of objectrequireditems 1 to 3
            • textstringrequiredA comment quoted VERBATIM from <comments>, character for character. Never paraphrase, never invent.
            • video_idstringrequiredvideo_id key (platform:id) of the video this comment was left under, exactly as given in <comments>.
          • anglestringrequiredA video someone could make that answers this ask, in one sentence.
      • lift_tablesobjectLift tables computed in code over the analysed videos: per visual format, hook type and length bucket, the video count, median views and engagement rate, and lift versus the whole set's median. The Digest quotes lifts only from these. Absent on research recorded before they existed.
        • setobjectrequired
          • video_countintegerrequired
          • median_viewsnumberrequirednullable
          • median_engagement_ratenumberrequirednullable
        • visual_formatarray of objectrequired
          • valuestringrequiredThe visual format, hook type, length bucket, or <format> + <hook type>.
          • video_countintegerrequired
          • median_viewsnumberrequirednullable
          • median_engagement_ratenumberrequirednullable(likes + comments + shares + saves) / views.
          • views_liftnumberrequirednullableMedian views of this row over the whole set's median views. Null when the set median is 0 or unknown.
          • engagement_liftnumberrequirednullable
          • low_confidencebooleanrequiredTrue when the row rests on fewer than 3 videos.
        • hook_typearray of objectrequired
          • valuestringrequiredThe visual format, hook type, length bucket, or <format> + <hook type>.
          • video_countintegerrequired
          • median_viewsnumberrequirednullable
          • median_engagement_ratenumberrequirednullable(likes + comments + shares + saves) / views.
          • views_liftnumberrequirednullableMedian views of this row over the whole set's median views. Null when the set median is 0 or unknown.
          • engagement_liftnumberrequirednullable
          • low_confidencebooleanrequiredTrue when the row rests on fewer than 3 videos.
        • lengtharray of objectrequired
          • valuestringrequiredThe visual format, hook type, length bucket, or <format> + <hook type>.
          • video_countintegerrequired
          • median_viewsnumberrequirednullable
          • median_engagement_ratenumberrequirednullable(likes + comments + shares + saves) / views.
          • views_liftnumberrequirednullableMedian views of this row over the whole set's median views. Null when the set median is 0 or unknown.
          • engagement_liftnumberrequirednullable
          • low_confidencebooleanrequiredTrue when the row rests on fewer than 3 videos.
        • format_hookarray of objectVisual format and hook type together, value as <format> + <hook type>: the rows the Digest's formulas take their lift from. Absent on research recorded before it existed.
          • valuestringrequiredThe visual format, hook type, length bucket, or <format> + <hook type>.
          • video_countintegerrequired
          • median_viewsnumberrequirednullable
          • median_engagement_ratenumberrequirednullable(likes + comments + shares + saves) / views.
          • views_liftnumberrequirednullableMedian views of this row over the whole set's median views. Null when the set median is 0 or unknown.
          • engagement_liftnumberrequirednullable
          • low_confidencebooleanrequiredTrue when the row rests on fewer than 3 videos.
      • videosarray of objectrequiredThe top 25 videos of the ranked shortlist, in rank order. Every video the research delivered pages, with filters and sorting, at GET /v1/research/{id}/videos.
        • platformstringrequired
        • idstringrequired
        • urlstringrequirednullable
        • descriptionstringrequired
        • posted_atstringrequirednullable
        • duration_secondsnumberrequirednullable
        • thumbnail_urlstringrequirednullable
        • hashtagsarray of stringrequired
        • soundobjectrequirednullable
          • idstringrequired
          • titlestringrequirednullable
        • creatorobjectrequirednullable
          • idstringrequired
          • handlestringrequirednullable
          • namestringrequired
          • follower_countnumberrequirednullable
        • snapshotobjectrequirednullable
          • atstringrequired
          • viewsnumberrequired
          • likesnumberrequired
          • commentsnumberrequired
          • sharesnumberrequired
          • savesnumberrequired
        • scoresobjectrequirednullableNull when the creator has no baseline yet.
          • outliernumberrequirednullableViews over the creator's median views.
          • outlier_p90numberrequirednullable
          • percentilenumberrequirednullableWhere this video sits in the creator's own sample, 0-100.
          • velocityobjectrequirednullable
            • views_per_hournumberrequired
            • windowstringrequired
          • reasonstringrequirednullablealways aged_outaged_out when the video is over 30 days old and Scores are null.
          • baselineobjectrequiredThe yardstick every Score on this creator is measured against.
            • median_viewsnumberrequired
            • p90_viewsnumberrequired
            • engagement_ratenumberrequirednullable
            • posts_per_weeknumberrequirednullable
            • sampleintegerrequired
            • confidencestringrequiredone of high, low
            • sourcestringrequiredone of videos, follower_tier
            • computed_atstringrequirednullable
            • stalebooleanrequiredTrue past 7 days; the next lookup refreshes it.
        • coveragestringalways partialPresent only where the platform's index is best-effort.
      • creatorsarray of objectrequiredThe top 25 creators, by best outlier multiple, then views. Every creator the research recorded pages, with filters and sorting, at GET /v1/research/{id}/creators.
        • platformstringrequired
        • idstringrequired
        • handlestringrequirednullable
        • namestringrequired
        • follower_countnumberrequirednullable
        • video_countintegerrequired
        • best_outliernumberrequirednullable
        • median_viewsnumberrequirednullableThe Creator baseline median views best_outlier was measured against, frozen at collection time. Null, never zero, when no video of theirs carried Scores.
        • total_viewsnumberrequired
        • video_idsarray of stringrequired
      • hashtagsarray of objectrequiredThe top 25 hashtags, by video count, then views. Every hashtag the research recorded pages, with filters and sorting, at GET /v1/research/{id}/hashtags.
        • valuestringrequired
        • titlestringnullable
        • platformsarray of stringrequiredEvery platform this tag was observed on in the shortlist. One rollup spans platforms; video_ids are platform:id if you want the split.
        • video_countintegerrequired
        • total_viewsnumberrequired
        • video_idsarray of stringrequired
      • soundsarray of objectrequiredThe top 25 sounds, by video count, then views. Every sound the research recorded pages, with filters and sorting, at GET /v1/research/{id}/sounds.
        • valuestringrequired
        • titlestringnullable
        • platformsarray of stringrequiredEvery platform this tag was observed on in the shortlist. One rollup spans platforms; video_ids are platform:id if you want the split.
        • video_countintegerrequired
        • total_viewsnumberrequired
        • video_idsarray of stringrequired
      • researchobjectrequired
        • requestedintegerrequired
        • completedintegerrequired
        • failedintegerrequired
        • cachedintegerrequired
        • freshintegerrequired
        • max_videosintegerrequired
        • included_videosintegerrequired
        • dropped_off_topicintegerCandidates judged off-topic and dropped before the shortlist; never charged.
        • dropped_unsureintegerCandidates whose relevance was unsure and that ranked too low to keep; never charged.
        • dropped_samplearray of objectUp to 10 dropped candidates, best-ranked first, with why each was dropped. Absent on research recorded before it existed.
          • video_idstringrequiredplatform:id of the dropped video.
          • verdictstringrequiredone of off_topic, unsure
          • reasonstringrequiredThe relevance check's reason, short.
  • Option 2objectReturned instead of a Run when estimate=true. Nothing is charged. Operations with more to say about a quote add members; the two above are always present.
    • estimate_creditsintegerrequiredWhat the call would be charged, in Credits.
    • cachedbooleanrequiredWhether a stored result can already answer. It does not change the price.

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_credits": 1036,
    "list_price_credits": 1036,
    "cached": false
  },
  "result": {
    "digest_version": 2,
    "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.",
          "template": "Day one I lasted [small number] [unit].",
          "lift": {
            "value": "STORY",
            "video_count": 9,
            "median_views": 1840000,
            "median_engagement_rate": 0.0912,
            "views_lift": 1.6,
            "engagement_lift": 1.21,
            "low_confidence": false
          }
        }
      ],
      "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."
        }
      ],
      "gaps": [
        {
          "ask": "Viewers want a cold plunge setup that does not cost hundreds.",
          "comments": [
            {
              "text": "ok but what do I do if I can't afford a 600 dollar tub",
              "video_id": "tiktok:7300000000000000000"
            }
          ],
          "angle": "A 30-day diary on a chest freezer or bathtub build, with the receipt on screen."
        }
      ]
    },
    "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
    }
  }
}

200 · With `estimate=true`: the price, no Run, no charge.

{
  "estimate_credits": 1036,
  "cached": false
}

202 Accepted

Still running: { run }. Poll GET /v1/runs/{id}.

object

Still running. Poll GET /v1/runs/{id}.

  • runobjectrequiredEvery charged operation creates a Run. The envelope is identical whether the result came back inline or was polled.
    • idstringrequired
    • kindstringrequiredWhat was run, e.g. video_lookup.
    • statusstringrequiredone of running, completed, partial, failedpartial keeps the completed items and charges for them, up to what the Balance covers; failed charges nothing.
    • created_atstringrequiredRFC 3339.
    • completed_atstringrequirednullableNull while the Run is still running.
    • charge_creditsintegerrequiredWhat this Run actually cost, in Credits. A stored answer costs the same as a fresh one.
    • list_price_creditsintegerrequiredThe full price of the work asked for, in Credits. Equal to charge_credits unless the Run charged for only part of it.
    • cachedbooleanrequiredTrue when a stored result answered without paying the vendor.
    • progressobjectWhat a running Research has done so far, refreshed as it works: stage (keywords, searching, filtering, analysing, writing_digest) and the counts known by then — keywords, found, kept, dropped_off_topic, and analysed of shortlisted. A count is absent until its stage produced it. Absent once the Run settles, and on every other kind.
      • stagestringrequiredone of keywords, searching, filtering, analysing, writing_digest
      • keywordsintegervalue at least 0
      • searchesintegervalue at least 0
      • foundintegervalue at least 0
      • judgedintegervalue at least 0
      • keptintegervalue at least 0
      • dropped_off_topicintegervalue at least 0
      • dropped_unsureintegervalue at least 0
      • shortlistedintegervalue at least 0
      • analysedintegervalue at least 0
      • updated_atstringrequired

202 · Example

{
  "run": {
    "id": "run_8f2c1ad04b",
    "kind": "research",
    "status": "running",
    "created_at": "2026-09-08T09:14:02.418Z",
    "completed_at": null,
    "charge_credits": 0,
    "list_price_credits": 0,
    "cached": false
  }
}

400 Bad Request

invalid_request (a malformed id, wait, estimate or Idempotency-Key) or unsupported_platform, refused before any Run opens. Nothing is charged.

object

RFC 9457 problem document, served as application/problem+json. Branch on code, never on the prose.

  • typestringrequired
  • titlestringrequired
  • statusintegerrequired
  • codestringrequiredone of missing_authorization, invalid_key, key_revoked, invalid_token, organization_not_selected, organization_access_revoked, connection_revoked, forbidden, invalid_request, unsupported_platform, unsupported_kind, kind_deprecated, not_found, video_too_long, idempotency_conflict, run_not_ready, advertiser_ambiguous, insufficient_balance, rate_limit_exceeded, concurrency_limit_exceeded, tracker_limit_exceeded, webhook_limit_exceeded, webhook_rotation_in_progress, collection_limit_exceeded, collection_name_taken, collection_kind_mismatch, media_unavailable, vendor_error, source_unavailable, service_unavailable, internal_errorStable machine-readable code.
  • detailstring

400 · Example

{
  "type": "https://www.shortsintel.com/docs/reference/errors/invalid_request",
  "title": "Invalid request",
  "status": 400,
  "code": "invalid_request",
  "detail": "`estimate` must be `true` or `false`."
}

401 Unauthorized

missing_authorization, invalid_key, key_revoked or invalid_token.

object

RFC 9457 problem document, served as application/problem+json. Branch on code, never on the prose.

  • typestringrequired
  • titlestringrequired
  • statusintegerrequired
  • codestringrequiredone of missing_authorization, invalid_key, key_revoked, invalid_token, organization_not_selected, organization_access_revoked, connection_revoked, forbidden, invalid_request, unsupported_platform, unsupported_kind, kind_deprecated, not_found, video_too_long, idempotency_conflict, run_not_ready, advertiser_ambiguous, insufficient_balance, rate_limit_exceeded, concurrency_limit_exceeded, tracker_limit_exceeded, webhook_limit_exceeded, webhook_rotation_in_progress, collection_limit_exceeded, collection_name_taken, collection_kind_mismatch, media_unavailable, vendor_error, source_unavailable, service_unavailable, internal_errorStable machine-readable code.
  • detailstring

401 · Example

{
  "type": "https://www.shortsintel.com/docs/reference/errors/invalid_key",
  "title": "Invalid key",
  "status": 401,
  "code": "invalid_key",
  "detail": "That API key does not exist. Keys look like `sk_live_...` and are created in the console."
}

402 Payment Required

insufficient_balance — the Balance cannot cover the quoted price (the worst case for Research and enriched search). No Run is created and nothing is charged. A multi-item Run whose Balance runs short later charges its completed items up to the Balance and ends partial.

object

RFC 9457 problem document, served as application/problem+json. Branch on code, never on the prose.

  • typestringrequired
  • titlestringrequired
  • statusintegerrequired
  • codestringrequiredone of missing_authorization, invalid_key, key_revoked, invalid_token, organization_not_selected, organization_access_revoked, connection_revoked, forbidden, invalid_request, unsupported_platform, unsupported_kind, kind_deprecated, not_found, video_too_long, idempotency_conflict, run_not_ready, advertiser_ambiguous, insufficient_balance, rate_limit_exceeded, concurrency_limit_exceeded, tracker_limit_exceeded, webhook_limit_exceeded, webhook_rotation_in_progress, collection_limit_exceeded, collection_name_taken, collection_kind_mismatch, media_unavailable, vendor_error, source_unavailable, service_unavailable, internal_errorStable machine-readable code.
  • detailstring

402 · Example

{
  "type": "https://www.shortsintel.com/docs/reference/errors/insufficient_balance",
  "title": "Insufficient balance",
  "status": 402,
  "code": "insufficient_balance",
  "detail": "Your balance does not cover this operation. Top up in the console.",
  "balance_credits": 0,
  "required_credits": 1036
}

409 Conflict

idempotency_conflict — the same Idempotency-Key with a different payload.

object

RFC 9457 problem document, served as application/problem+json. Branch on code, never on the prose.

  • typestringrequired
  • titlestringrequired
  • statusintegerrequired
  • codestringrequiredone of missing_authorization, invalid_key, key_revoked, invalid_token, organization_not_selected, organization_access_revoked, connection_revoked, forbidden, invalid_request, unsupported_platform, unsupported_kind, kind_deprecated, not_found, video_too_long, idempotency_conflict, run_not_ready, advertiser_ambiguous, insufficient_balance, rate_limit_exceeded, concurrency_limit_exceeded, tracker_limit_exceeded, webhook_limit_exceeded, webhook_rotation_in_progress, collection_limit_exceeded, collection_name_taken, collection_kind_mismatch, media_unavailable, vendor_error, source_unavailable, service_unavailable, internal_errorStable machine-readable code.
  • detailstring

409 · Example

{
  "type": "https://www.shortsintel.com/docs/reference/errors/idempotency_conflict",
  "title": "Idempotency conflict",
  "status": 409,
  "code": "idempotency_conflict",
  "detail": "This Idempotency-Key was used with a different payload.",
  "run_id": "run_8f2c1ad04b"
}

429 Too Many Requests

rate_limit_exceeded or concurrency_limit_exceeded, with Retry-After.

object

RFC 9457 problem document, served as application/problem+json. Branch on code, never on the prose.

  • typestringrequired
  • titlestringrequired
  • statusintegerrequired
  • codestringrequiredone of missing_authorization, invalid_key, key_revoked, invalid_token, organization_not_selected, organization_access_revoked, connection_revoked, forbidden, invalid_request, unsupported_platform, unsupported_kind, kind_deprecated, not_found, video_too_long, idempotency_conflict, run_not_ready, advertiser_ambiguous, insufficient_balance, rate_limit_exceeded, concurrency_limit_exceeded, tracker_limit_exceeded, webhook_limit_exceeded, webhook_rotation_in_progress, collection_limit_exceeded, collection_name_taken, collection_kind_mismatch, media_unavailable, vendor_error, source_unavailable, service_unavailable, internal_errorStable machine-readable code.
  • detailstring

429 · Example

{
  "type": "https://www.shortsintel.com/docs/reference/errors/rate_limit_exceeded",
  "title": "Rate limit exceeded",
  "status": 429,
  "code": "rate_limit_exceeded",
  "detail": "60 requests per minute per key. Retry after the number of seconds in `Retry-After`.",
  "limit": 60
}

502 Bad Gateway

A Run that opened and then failed is answered as application/problem+json with its problem's status — vendor_error here, not_found as 404 — and the Run envelope alongside, so the Run id is kept. A failed Run is never charged.

object

RFC 9457 problem document, served as application/problem+json. Branch on code, never on the prose.

  • typestringrequired
  • titlestringrequired
  • statusintegerrequired
  • codestringrequiredone of missing_authorization, invalid_key, key_revoked, invalid_token, organization_not_selected, organization_access_revoked, connection_revoked, forbidden, invalid_request, unsupported_platform, unsupported_kind, kind_deprecated, not_found, video_too_long, idempotency_conflict, run_not_ready, advertiser_ambiguous, insufficient_balance, rate_limit_exceeded, concurrency_limit_exceeded, tracker_limit_exceeded, webhook_limit_exceeded, webhook_rotation_in_progress, collection_limit_exceeded, collection_name_taken, collection_kind_mismatch, media_unavailable, vendor_error, source_unavailable, service_unavailable, internal_errorStable machine-readable code.
  • detailstring

502 · Example

{
  "type": "https://www.shortsintel.com/docs/reference/errors/vendor_error",
  "title": "Vendor error",
  "status": 502,
  "code": "vendor_error",
  "detail": "The upstream data provider could not be reached.",
  "run": {
    "id": "run_8f2c1ad04b",
    "kind": "research",
    "status": "failed",
    "created_at": "2026-09-08T09:14:02.418Z",
    "completed_at": "2026-09-08T09:14:04.930Z",
    "charge_credits": 0,
    "list_price_credits": 0,
    "cached": false
  }
}

503 Service Unavailable

source_unavailable: every source this Run reads failed or is switched off. The Run fails uncharged, with its envelope alongside; endpoints names the source. Retry later rather than treating it as empty.

object

RFC 9457 problem document, served as application/problem+json. Branch on code, never on the prose.

  • typestringrequired
  • titlestringrequired
  • statusintegerrequired
  • codestringrequiredone of missing_authorization, invalid_key, key_revoked, invalid_token, organization_not_selected, organization_access_revoked, connection_revoked, forbidden, invalid_request, unsupported_platform, unsupported_kind, kind_deprecated, not_found, video_too_long, idempotency_conflict, run_not_ready, advertiser_ambiguous, insufficient_balance, rate_limit_exceeded, concurrency_limit_exceeded, tracker_limit_exceeded, webhook_limit_exceeded, webhook_rotation_in_progress, collection_limit_exceeded, collection_name_taken, collection_kind_mismatch, media_unavailable, vendor_error, source_unavailable, service_unavailable, internal_errorStable machine-readable code.
  • detailstring

503 · Example

{
  "type": "https://www.shortsintel.com/docs/reference/errors/source_unavailable",
  "title": "Source unavailable",
  "status": 503,
  "code": "source_unavailable",
  "detail": "The tiktok/video source is unavailable right now.",
  "endpoints": [
    "tiktok/video"
  ],
  "run": {
    "id": "run_8f2c1ad04b",
    "kind": "research",
    "status": "failed",
    "created_at": "2026-09-08T09:14:02.418Z",
    "completed_at": "2026-09-08T09:14:04.930Z",
    "charge_credits": 0,
    "list_price_credits": 0,
    "cached": false
  }
}

Request samples

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}'

This call creates a Run. With wait it holds the response open until the Run finishes or the wait runs out, then answers with the finished result or 202 and the running Run. Poll GET /v1/runs/{id} until it settles; re-reading a Run is free.

  • Runs: How a call becomes a Run, waiting inline, polling and free re-reads.
  • Pricing and estimates: Credits, estimates and why a cached result costs the same as a fresh one.
  • 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.