SocialCrawl

Discover

Search many platforms for a topic, keep a date window, judge every row and get one de-duplicated list of posts

GET /v1/prism/discover searches several platforms at once for your keywords, keeps only the posts published in your date window, judges every row against your question, and removes duplicates. A small request answers in the same call. A large one continues in the background and answers 202 with a run you can follow, so one call always works.

Your first call

This is a chat agent's persona question: the topic words go in query, the question goes in context, and depth=quick keeps it to a few fast platforms.

cURL
curl -G "https://www.socialcrawl.dev/v1/prism/discover" \
  -H "x-api-key: YOUR_API_KEY" \
  --data-urlencode "query=electric cars" \
  --data-urlencode "context=What do women over 50 say about owning an electric car?" \
  --data-urlencode "depth=quick" \
  --data-urlencode "period=90d"
Response
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "nw_akd38616exek",
        "platform": "news",
        "record_type": "article",
        "url": "https://www.motortrend.com/reviews/2025-acura-adx-yearlong-review-update-6-ev-switch-gas-pros-cons",
        "published_at": "2026-08-05T07:00:00.000Z",
        "in_window": true,
        "text": "I Switched Back to Gas After Years of EVs ...",
        "judgments": { "relevance": { "p": 0.94 } },
        "matched_keywords": ["electric cars"],
        "provenance": { "endpoint": "/v1/google_news/search", "request_id": "req-…", "captured_at": "…" }
      }
    ],
    "run": {
      "run_id": "run_EpfL4dXi6qpYZCBRNiybdK",
      "status": "completed",
      "stopped_because": null,
      "deduplicated": false,
      "credits": { "spent": 8, "by_stage": { "collect": 3, "judge": 5 } },
      "next_step": "The run is complete with 10 rows here and 10 kept in total at next.results_url; ..."
    },
    "next": {
      "status_url": "https://www.socialcrawl.dev/v1/prism/discover/runs/run_EpfL4dXi6qpYZCBRNiybdK?wait_s=25",
      "results_url": "https://www.socialcrawl.dev/v1/prism/discover/runs/run_EpfL4dXi6qpYZCBRNiybdK",
      "continue_url": "https://www.socialcrawl.dev/v1/prism/discover?query=electric+cars&context=What+do+women+over+50+say+about+owning+an+electric+car%3F&depth=quick&period=90d&max_credits=50&continue=…",
      "resume_url": null
    },
    "platforms": { "youtube": { "status": "ok" }, "tiktok": { "status": "ok" }, "news": { "status": "ok" } },
    "summary": { "rows": { "returned": 178, "kept": 10, "dropped": 166, "duplicate": 2 } },
    "request": { "since": "2026-07-19", "until": "2026-10-16", "tz": "UTC" }
  },
  "pagination": { "next_cursor": null, "has_more": false },
  "credits_used": 8
}

Every request is a run with a run_id. Each platform search runs on that platform's own search endpoint and bills its own page price. credits_used is the run's total: the searches plus any judging, topic card, translation and page bodies, and data.run.credits.by_stage splits it. data.run.next_step is one plain sentence that tells you, or your agent, what to do next.

A brand's month, in one call

Canonical request 1 measures the reaction to three brands over the previous calendar month in New York time. Each brand becomes its own group, labelled with the mention judgment.

cURL
curl -G "https://www.socialcrawl.dev/v1/prism/discover" \
  -H "x-api-key: YOUR_API_KEY" \
  --data-urlencode "brands=aruba,curacao,bonaire" \
  --data-urlencode "preset=travel" \
  --data-urlencode "period=last_month" \
  --data-urlencode "tz=America/New_York" \
  --data-urlencode "exclude_terms=networks" \
  --data-urlencode "max_credits=150"

exclude_terms=networks drops rows about Aruba Networks. max_credits=150 is the most this run may spend: it pauses there and never overspends, and a request expected to cost more, or with one search that could cost more on its own, is refused free before anything is spent.

How long does a call take?

Every platform search is a real call to that platform, so it takes seconds per platform call, and the searches run in parallel. A request that fits one call answers within deadline_s (40 seconds by default, 15 with depth=quick, at most 96). A background run of many keywords and pages takes minutes per run, and a very large one takes hours. Set your client timeout to deadline_s plus 10 seconds, so 50 seconds by default and 25 seconds with depth=quick.

Is a keyword an exact match?

No, and not a wildcard either. Each keyword is sent to every platform's own search, which matches words the way that platform does: most return posts containing the words or close variants, ranked by the platform. With expand=1 to 3, discover also plans extra searches from your keywords and context. Then every row is read by the judge against your keywords, or against context when you send one, so rows about something else are dropped under relevance=filter. literal_match on a row says whether a keyword appears in its text as written, and exclude_terms and contains filter on the text exactly.

One call or a background run?

A request answers in the call when it fits one call's budget: at most 6 keywords, at most 36 platform searches (pages to reach a past window included, and every search counted 1 + expand times), at most 3 pages_per_platform, at most 50 joined rows (include=author_country or instagram_views each join up to limit rows per group), a limit within the call's cap, one view, and no depth=deep. Anything larger continues in the background (async=auto, the default) and answers 202 with the run and empty data.items. Send async=true to always run in the background, or async=false to get NEEDS_ASYNC (422, free) instead of a 202. GET /v1/prism/discover?...&dry_run=true answers the plan of searches and the estimate of credits and seconds, free, and starts nothing.

Lists and groups too long for a URL go in the JSON body of POST /v1/prism/discover, which takes the same fields, requires max_credits unless dry_run is true, and always runs in the background. A body over 1 MB is a free 413.

A long author list

POST /v1/prism/discover/inputs with { "kind": "authors", "authors": ["nasa", "@natgeo", "twitter:spacex"] } stores 1 to 10,000 handles (each at most 100 characters) and answers 201 with author_list_id, which any run takes in place of authors. A bare handle matches on any platform and platform:handle on that platform only. A list is free, belongs to your account, and expires after 30 days; an account holds at most 20 at once, and a 21st is a free 400 input_limit. authors itself takes at most 100 handles on the GET and 1,000 on the POST.

Run statuses

data.run.status is one vocabulary for every run:

StatusMeaningWhat to do
queuedWaiting: your account already runs two background runs.Wait on next.status_url.
runningCollecting, judging or delivering.Wait on next.status_url; read rows as they arrive.
pausedStopped by max_credits, the key's credit limit, the balance or an outage of our own.Relay next_step to the user, then call next.resume_url. A pause for our own outage resumes by itself, usually within about an hour.
partialFinished with something missing: a failed platform, unjudged rows, the deadline, or a revoked key (key_revoked).Use the rows; call next.resume_url if the rest is needed.
completedFinished.Read every kept row at next.results_url, free.
failedNothing could be collected.Retry later, or drop the failing platforms.
cancelledStopped by DELETE; the rows are kept.Call next.resume_url to continue.

stopped_because says why a run stopped: time, max_credits, key_budget, insufficient_credits, upstream_failures, service_unavailable, internal_error, cancelled or key_revoked. A partial run held up only by judging names time, service_unavailable or max_credits, and its waiting rows are marked judgment_status: "pending" and counted in data.run.legs.judging_pending. data.run.progress.rows_kept counts judged kept rows.

A run paused with service_unavailable (an outage of ours) is resumed automatically about 2, 5, 15, 60, 60 and 60 minutes after each pause, at most 6 times per outage (a resume you send, or a step of the run that completes, starts the count again); after that its next_step asks you to resume it. A pause for insufficient_credits, key_budget or max_credits never resumes by itself, because it needs your decision. A paused run's next_step also says the date to resume before, when the run and its rows expire.

In the background a run searches up to 30 platform pages at once, and rows become readable as each background step of up to 50 seconds commits. An account runs two background runs at once; a third answers 202 with status: queued and starts by itself when one of the two settles.

Never pay twice

An identical request while its run is queued, running or paused returns that run (deduplicated: true); a paused run comes back with a next_step naming resume. An identical request within 24 hours of a completed run returns its stored result, free. An identical GET within 24 hours of a partial GET returns that run free, with a next_step naming resume. "Identical" means the same subject, sources, window, filters, judging, views, depth, expansion, pages and data-changing include values. Fields that only shape the answer or the spend do not count: limit, limit_per_keyword, format, fields, author_identity, sort, sample, max_per_author, max_per_platform, max_credits, deadline_s, async, retention_days, fresh, cursor and page_size. A stored run is matched only when it was asked for at least as many rows: the same or a lower limit returns it free, and a higher limit starts a new, billed run. A higher max_credits alone still returns the stored run free; when that run is paused (or is a partial GET), its next_step says to resume it with the higher cap. A partial POST run is never matched. Send new_run=true to start a fresh run anyway.

Reading a run

Reading a run is free. Copy the URLs in data.next; never assemble them.

CallWhat it does
GET /v1/prism/discover/runsYour runs from any of your keys, newest first (status, kind, created_after, cursor, page_size up to 100). Use it when you lost a run_id.
GET /v1/prism/discover/runs/{run_id}?wait_s=25Status, progress and a page of rows. wait_s (up to 25) answers early on a status change, a finished stage or 10% more rows.
GET /v1/prism/discover/runs/{run_id}?changes_since={version}Only rows added or changed since the data.run.version you hold.
GET /v1/prism/discover/runs/{run_id}/results.jsonlEvery row, one JSON object per line, streamed. The last line is {"complete":true}; when the export stops early (after about 45 seconds) it is {"next_cursor":"…"} instead, and ?cursor= with that value and the same filters continues it.
GET /v1/prism/discover/runs/{run_id}/legsEvery search the run made, with its platform, search text, page, status, request_id and credits, up to 500 a page.
POST /v1/prism/discover/runs/{run_id}/resumeContinues a paused, partial, failed or cancelled run from its stored searches and answers 202. Body {} or { "max_credits": 200, "retry_failed": true }, nothing else. Free.
DELETE /v1/prism/discover/runs/{run_id}Cancels a run. Its rows stay readable and resume continues it. A run that already settled answers 409 RUN_STATE_CONFLICT.

The run reads filter for free with group, tag.<key>=<value>, platform, keyword, state=kept, dropped or all, include=dropped and include_pending=true, and a settled run also takes sort; sort or order=rank on a running run is the free 400 order_rank_while_running. A key holds at most 5 waits (wait_s) at once, and a sixth answers at once as if wait_s=0, so wait on one run at a time. view must be the run's own view. A run route refuses a parameter it does not take with a free 400, and DELETE and resume take none. While a run is running its rows come in arrival order, so a cursor never skips a row; once it is settled, order=rank is the default. A cursor keeps the order and the filters it was issued with: one taken while the run was running keeps paging in arrival order after it completes, and sending it with other filters, or an order that contradicts it, is a free 400.

A resume of a queued or running run answers 200 and starts nothing; two resumes at the same moment start the run once. A resume of a completed run is 409 RUN_STATE_CONFLICT. A run paused at its cap and resumed without a higher max_credits is a free 400 whose next_step suggests a cap. A partial GET run resumes as a background run. Only searches that are not done run again (with retry_failed, failed and skipped ones too): a finished search is never fetched again, and no stored row is removed or gets a new id. A search whose platform cursor expired is walked again from its first page; those pages bill again, only rows not already stored are kept, and _warnings says so. A resume extends the run's expiry to at least today plus its retention_days. A resume also judges the kept rows that were not judged yet, and only those. A resume answers 202 with the run queued instead of running when your account already runs two background runs; it starts when one of them ends. Searches of a failed GET that were refunded run again on resume and bill again; their earlier rows are never served free.

When extra searches (expand) cannot be planned in time for some keywords, those keywords run alone and the run settles partial with stopped_because: service_unavailable. _warnings (on the GET) and data.run.limits say so, and a resume with { "retry_failed": true } plans the extra searches for those keywords only; searches already fetched never run again.

Some failures repair themselves. On a background run, a platform skipped after repeated failures is tried once more 5 minutes later, and its skipped searches run if that try works. A 402 that arrives within about 60 seconds of a top-up, while the balance does cover the run, is retried once after 60 seconds instead of pausing the run. When a platform is unavailable at the source (out of quota or down), data.platforms.<platform>.status is unavailable and next_step names the platform and says a resume will fail until it returns. data.run.credits.pending_refund shows credits charged to searches whose background step died before it finished; the nightly reconciliation pays them back, usually within a day.

pagination.next_cursor on the GET pages the run's stored rows, free, from its first kept row, so the next pages include the rows the GET already delivered. When the run store could not be written, the answer carries _warnings: resume_unavailable and no cursor. Buying more from the platforms is never a cursor. On the answer of a GET that finished in the call, data.next.continue_url buys the next pages of the searches that reported more, with data.next.continue_estimate; a background run walks its pages_per_platform instead, and its answers and run reads carry continue_url: null. Each continue_url buys its pages once, and sending it again returns the stored run free with deduplicated: true. The max_credits you send with a continue becomes the run's cap, so a run that spent 45 of 50, continued with max_credits=60, may spend 15 more. As offered, continue_url carries a max_credits large enough to buy every page it estimates; edit it down to spend less, and the continue then pauses at that cap without buying anything twice.

The summary

data.summary holds counts only, each under an object that names its base, read from the stored rows: kept, dropped by reason, inside and outside the window, undated, per group, keyword, platform and language, the funnel and credits_per_kept. Every summary carries as_of_version and stale. While a run is queued or running the summary is refreshed as it progresses, about every 10 seconds at most, so a read can return counts as of an earlier version than the page: then stale is true, as_of_version is below data.run.version, and data._warnings has a line starting summary_stale:. The counts of a stale summary agree with each other as of that version. A run of 50,000 rows or more read before its first summary exists answers summary: null with a summary_pending: warning. Once a run settles, its summary is exact (stale: false).

The request

Tier 1 fields are what most calls need. Tier 2 fields shape the rows. Tier 3 fields are for background runs and experts. Every field is strict: an unknown field, a contradiction, or a field from a later release is a free 400 that names the field to change.

Tier 1

FieldDefaultMeaning
keywordsnoneThe searches, comma separated; each runs once on each platform. Up to 6 run in one call.
querynoneOne search phrase, never split on commas.
contextthe keywordsWhat kept rows must be about, 3 to 2,000 characters. A persona or a question goes here, never in keywords.
brands (alias brand)noneEach brand is its own group, searched by its name and labelled with the mention judgment.
platforms or presetpreset=consumerThe platforms, in order, or a named preset.
period, or since and until (aliases from, to)noneNd (1 to 365), yesterday, this_week, last_week, this_month, last_month, last_quarter, this_year, ytd; or dates YYYY-MM-DD, inclusive. tz sets the calendar (default UTC).
viewpostsWhat the run returns.
limit50 on the GET (quick 20), 500 on the POSTKept rows per group; at most 100 in one call, which answers at most 300 rows in total and pages the rest, and 5,000 in a background run.
max_credits50 on the GETThe most the run may spend. Required on the POST unless dry_run is true.
depthstandardquick (at most 3 fast platforms, 15 seconds) or deep (background).
dry_runfalseThe plan and the estimate, free.

Tier 2

FieldMeaning
country, exclude_country, unknown_countryWhere rows or authors are, on the row's geo object; country also picks the news edition, and a country of the Arab League plans searches in en and ar unless you send languages.
languagesRows in these languages; searches are planned in them.
min_views, min_likes, min_comments, min_followers, max_followers, verified, media_type, link_domainsRow and author filters, applied by code before anything is judged. media_type is video, image, carousel or text.
exclude_terms, containsDrop rows with a term; keep only rows containing a string or URL prefix (without keywords, contains is searched).
undated, estimated_dates, outside_window, min_text_lengthHow undated rows, estimated dates, rows outside the window and very short rows are handled.
sitesDomains to search inside on the web source, up to 20. web is added when you send no platforms or preset; a platform list or preset without web is a free 400.
relevance, relevance_thresholdfilter (default), score or off; the threshold is 0.5 by default.
label, intentJudgments to add (mention, intent, niche, sponsored_model, or none), each billed; label=mention needs brands (or a group brand). intent keeps rows of these intents (switching, complaint, recommendation, question, purchase_intent, selling) and adds the billed intent judgment.
sponsoredkeep (default), exclude or only rows flagged or disclosed as sponsored.
limit_per_keyword, sort, sample, max_per_author, max_per_platformHow many rows per keyword, in which order (relevance, recent, engagement, views or views_per_follower), and a representative sample.
includeauthor_country, body (full text of the top kept articles, pages and threads, with body_limit: default 10, at most 50 on the GET and 5,000 on the POST; a page behind a block or challenge is retried on stronger tiers, which can bill up to 5 credits), raw, instagram_views, dropped.
format, fieldsevidence (default), compact or text; fields keeps only the keys you name.
author_identityfull (default), omit or hash. It hides handles in every rendered field, but include=raw returns each platform's own row unchanged, handle included: for rows you show to your own users, send omit or hash and leave include=raw off.
translate_toAdds text_translated in this language.
continue, since_watermarkThe token from data.next.continue_url; a watermark from an earlier run returns only newer rows.

Tier 3

FieldMeaning
groupsPOST only: up to 20 groups, each with its own id, keywords, context, brand, exclude_terms, platforms or preset, search_in, languages, country, sites and tags. Rows carry their group and group_tags.
authors, author_list_id, authors_matchKeep only rows by these authors.
hashtags, search_inHashtag searches; lane kinds per platform (Reddit comments, Naver's corpora, web sites).
expand0 (default) to 3 extra searches planned per keyword.
pages_per_platform1 (default) to 10; above 3 the run continues in the background.
retention_days1 to 30 (default 7). After it, a finished run answers RUN_EXPIRED on every route; a queued, running or paused run can still be read and cancelled.
deadline_s, asyncThe GET's time budget (10 to 96 seconds), and whether it may continue in the background.
fresh, new_run, excludeSkip cached search pages at full price, start a new run, and leave out ids you already hold.
<platform>.<param>A platform search's own parameter, such as tiktok.region or instagram.surface=relevance.
cursor, page_sizeGET only: pagination.next_cursor of an earlier answer pages that run's stored rows, free, page_size (1 to 100, default 50) at a time.

Only new rows: since_watermark

Every run answers data.next.watermark, an opaque token bound to your account and to the request without its resolved dates, so period=7d keeps working day after day. A GET answers it in the call; a background run answers it on its run route once it settles. Send it back as since_watermark on the same request and you get only rows that earlier runs of that request did not deliver. Rows already delivered are dropped before judging, so they are never billed again, while the platform searches are paid as usual. summary.new_since_last_run counts the kept rows the previous run had not delivered, and is null on the first run.

A request with since_watermark always fetches: it never returns a completed run, and two identical ones sent while the first is running share that run. A token from another account or another request is a free 400 watermark_mismatch, and when the watermark cannot be read the call answers a free, retryable 503 (store_unavailable, or watermark_unavailable when it cannot be checked at all) rather than running without it. A platform's watermark moves only when all its searches in the run finished, never past the run's start; rows a paused, cancelled or revoked-key run already made readable count as delivered. Kept rows still waiting for the judge (on a partial run with judging left, or a run paused at max_credits) do not count, and their platform's watermark does not move, so the next run returns them once they are judged. Right after a background run settles, data.next.watermark can read null for a moment until the watermark is written; reading the run again a second later shows it. Ids delivered within 3 days of the watermark are remembered, so a late-indexed post still comes through once.

Canonical requests

#RequestAvailable
1Brand reaction for a month: brands=aruba,curacao,bonaire&preset=travel&period=last_month&tz=America/New_York&exclude_terms=networks&max_credits=150Today
2A chat agent's persona question: query=electric cars&context=What do women over 50 say about owning an electric car?&depth=quick&period=90dToday
3Sellers behind a topic: view=accounts&role=seller,reseller&targets_country=AERel3
4Creators in a country: view=accounts&preset=creators&country=ES&min_followers=50000&include=contactsRel3
5Trends with no keyword: view=items&country=DE&period=14dRel5
6Communities: view=communities&platforms=reddit,facebook,telegramRel5
7Links or referral codes: contains=claude.ai/referral/&platforms=twitter,reddit,threads,github,web&relevance=off&period=7d, then the same with since_watermark on every later runToday
8Market research for a client: a POST with one group per topic and market, tagged, with include=dropped and bodyToday, in the research guide

A request that needs a later release answers a free 400 not_released naming the release in details.release; on the GET, details.suggested_url is the nearest request that works today.

Presets

PresetPlatforms, in orderUse it when
consumeryoutube, reddit (posts and comments), tiktok, twitter, threads, newsWhat people say about a topic, product or brand (the default).
b2blinkedin, reddit, news, web, twitter, hackernewsBusiness and professional topics.
travelinstagram, tiktok, youtube, reddit, pinterest, newsDestinations, hotels and trips.
appsreddit, tiktok, youtube, twitter, newsReactions to an app or a software product.
newsnews, web, reddit, twitter, hackernewsCoverage and discussion of an event or a company.
commercetiktok, instagram, youtube, reddit, pinterestProducts and shopping.
creatorstiktok, instagram, youtubeShort-video and creator platforms only.
koreanaver, youtube, tiktok, instagram, threads, news, webKorean-language research.

The consumer set was chosen by measuring in-window, on-topic kept rows per credit on 16/10/2026: YouTube 5.4, Reddit comments 3.2, Reddit posts 2.4, TikTok 2.4, X 2.0, Threads 1.9, news 1.6 and Instagram 0.25, so Instagram is left out of the default.

How far back can each platform reach?

platforms takes tiktok, instagram, youtube, reddit, twitter (or x), threads, facebook, linkedin, pinterest, bluesky, hackernews, forums, quora, github, news, web and naver. Each platform's own search decides how far back a window can reach. data.platforms.<platform>.window reports what each search actually reached, with reached: false, rows_outside and stopped_because when it stopped before since. Pages walked only to reach a past window count in the call's budget and are priced in the estimate. data.platforms.<platform>.status is ok, degraded (some searches failed or were skipped), failed (every search failed), skipped (none ran: the platform's circuit was open or no slot was free), unavailable (unavailable at the source: out of quota or down) or unsupported_country (the platform does not serve the requested country).

PlatformPrice per search pageHow the window is appliedReach
TikTok1 creditThe narrowest date bucket covering since, newest first; when page 1 is entirely after until it switches to the relevance bucket and filters locally.A date page covers about 2 to 3 days, so a window closed up to about a week back is reached directly; up to 180 days through the relevance bucket. Older than 180 days is not reached.
Instagram1 credit (hashtags 5)Inside 90 days the relevance surface first, switching to the date surface when page 1 holds under 15% in the window; beyond 90 days the date surface. instagram.surface=relevance never switches.365 days at about 9 reels a page on the date surface. Views are empty on the date surface; include=instagram_views restores them.
YouTube1 creditExact dates.Any window.
Reddit1 creditThe narrowest time bucket that misses at most 2 days, with pages.Any window, with pages.
X1 creditExact dates, newest first.Any window, with pages.
Facebook, Threads1 creditExact dates.Any window.
LinkedIn5 credits (hashtags 10)Date order with pages; switches to relevance when page 1 is after until.A page of 10 posts covers under a day for a busy keyword, so keyword search reaches only the last few days.
Pinterest1 creditPages filtered locally.Rows of any age arrive in relevance order, so pages add in-window rows at a steady low rate.
Hacker News, news1 creditExact dates.Any window; news returns at most 100 rows a call.
Bluesky, GitHub1 creditPages filtered locally.Rows of any date the search returns; only dated rows count in the window.
Quora5 creditsPages filtered locally.Rows of any date the search returns; only dated rows count in the window.
Forums10 creditsOne fused search across forums, filtered locally.Rows of any date the search returns; only dated rows count in the window.
Web2 credits per 10 resultsExact or bucketed dates from the group's market; only dated rows count in the window.Any window, where the page publishes a date.
Naver blog, news, KnowledgeiN, cafe1 creditThe best 100 rows by relevance, then one newest-first page with a window.By default, with a window, the top 100 plus one newest-first page; with pages_per_platform=10, up to the newest 1,000 rows per search. On a common word that is days, so narrower keywords reach further. Cafe rows are undated.

What it costs

ItemPrice
A platform searchThe platform's own page price (table above), only when it returns rows. A search that fails, finds nothing or is served from cache costs 0.
Relevance against your keywordsFree. Judging is counted per group and on fresh rows only: a verdict already in the cache costs nothing.
Relevance against a context that differs from the keywords1 credit per started 15 judged rows.
Each label value1 credit per started 25 judged rows, each.
A topic card (a context over 200 characters)1 credit.
translate_to1 credit per started 1,000 translated characters.
sitesPer site and keyword, page 1 is two searches: a site search of up to 10 results (1 credit) and one search of a second web index limited to the site, shown as /v1/web/search (second index) in the legs list and in row provenance (1 credit for up to 20 results; it is not the public /v1/web/search price). Each further page (pages_per_platform above 1) is one more site search at 1 credit.
include=bodyThe page read's own price per body. Login walls, blocks, challenges and empty pages cost 0; paywall previews, consent walls and pages that turn out to be lists are billed as served.
Filters, geo, the sponsored verdict, de-duplication, summaries, stored inputs, watermarks, replays, include=dropped, and reading a runFree.

A request is refused free with max_credits_below_estimate and details.suggested_max_credits when its expected cost is above max_credits, or when one search can cost more than max_credits on its own, and with a 402 when your balance is below the lower of max_credits and the estimate. One case answers 402 before any of that: the router holds the judging and executor part of the estimate before discover's own free checks, so a balance under that hold gets the 402 even where an identical run would have been returned free. Topping up or lowering max_credits clears it.

Errors

Every error uses the standard envelope with details.reason and details.next_step. partial and paused are not errors: they are 200 answers with data.run.

CodeStatusWhen
INVALID_REQUEST400invalid_param (also an invalid or mismatched cursor), conflicting_params, not_released (with suggested_url on the GET), post_only_field, max_credits_below_estimate, order_rank_while_running, watermark_mismatch, true_limit, input_limit.
INSUFFICIENT_CREDITS, KEY_BUDGET_EXCEEDED402The balance or the key's credit limit is below what the run needs.
RUN_NOT_FOUND404No run with this id belongs to your account.
RUN_STATE_CONFLICT409The run's state does not allow the call, for example resuming a completed run.
RUN_EXPIRED410A finished run's retention passed, or the run was purged.
PAYLOAD_TOO_LARGE413The URL is too long (send a POST, or store long author lists), or a POST body is over 1 MB.
NEEDS_ASYNC422Only with async=false: the request is over one call's budget. details.reason is legs, joins or limit (comment_posts arrives with comments in Rel4), details.over lists every one, and the fields behind them are in details.causes.
UPSTREAM_ERROR502all_legs_failed: nothing was collected, and nothing was charged.
SERVICE_UNAVAILABLE503store_unavailable, queue_unavailable, idempotency_store_unavailable or watermark_unavailable (since_watermark cannot be checked right now); retry after Retry-After. A POST refused with queue_unavailable leaves its run failed without running or charging it, so the same request starts a new run.

GET /v1/utility/explain-error explains any of these from the pasted response, free.

Compared with search/multi and search/everywhere

/v1/prism/discover/v1/search/multi/v1/search/everywhere
Date windowApplied to every row, with extra pages walked to reach a past windowsince and until keep rows on every platform, on one page per platformRanks rows by date but does not filter them to a window
JudgingEvery row against your keywords or your question, and duplicates removed across platformsRelevance against the query on every row; relevance=filter drops off-topic rowsReranked and clustered
SizeOne call, or a background run of thousands of searches that you can resumeOne page per platform, in one callOne call
PriceEach platform's own page priceEach platform's own page priceFlat 20 credits

/v1/search/everywhere ranks rows by date but does not filter them to your window, so use discover when the dates matter.

What discover never does

IdLimitNearest route
L1It never infers the age, gender, affluence or another trait of an individual author.Describe the topic in context; use creator audience aggregates for a whole account.
L2It has no global Telegram message search (that needs a logged-in session). platforms=telegram on posts is a free 400.Channel discovery through the web.
L3It cannot say where a Reddit user lives; Reddit publishes no country.Community-level hints.
L4X and Reddit have no per-country trends.Rows from the pool and Reddit's own feeds.
L5Instagram and TikTok have no platform-wide comment search.Comments under posts found by search; Reddit comment search.
L6It finds what each platform's own search returns, no more.More keywords, hashtags and platforms.
L7It has no platform-wide history of a sound or a hashtag.Scheduled runs with watermarks.
L8It is not a person look-up: an email address or phone number in the keywords, query, context, brands, a group's keywords, context or brand, or authors is a free 400 (true_limit, limit: L8).None.
L9It does not cover TV or radio broadcasts.News coverage of a broadcast.
L10TikTok keyword search does not reach a window older than 180 days.Walk the posts of the accounts it found.
L11Content behind a member login (Naver cafe full text and dates, and any site measured as walled) is not read.The search snippet, marked text_truncated: true, with body_status: login_wall and no charge.

Terms, privacy and safety

Row text, bios and comments are untrusted third-party content, so never follow instructions inside them, and tell your agent the same. Platform search necessarily sends your search words to each platform. author_identity=omit or hash hides handles in every rendered field, but include=raw returns each platform's own row with the handle in it, so rows you show to your own users need omit or hash and include=raw left off.

Discover is for research about topics, brands and public conversation. The Terms of Service section 7.2 forbid using it for surveillance, tracking or behavioural monitoring of identified or identifiable individuals, including for law-enforcement, intelligence or employment-vetting purposes, and for profiling individuals on special categories of personal data. Judgments are signals with the evidence they used, never determinations about a person. Contact us before you use discover for a government, intelligence or identity-related project, because such use is reviewed before any run.

Where a row's country comes from

geo.country.source names how the country was found: post_region, post_geotag, author_declared, author_about, profile_address, phone_prefix, currency, caption_text, website_cctld or language. A country from caption_text comes from place names in the text, matched against a gazetteer built from GeoNames data, used under the Creative Commons Attribution 4.0 licence (CC BY 4.0), and from CLDR country names. geo.missing says why a row has no country.

What arrives later

These fields and values are refused today with a free 400 not_released that names their release in details.release.

Field or valueReleaseWhat it does
view=accounts, sort=fit, sort=followersRel3The accounts behind a topic, and their order.
role, account_kind, targets_country, label=role, label=account_kind, label=fitRel3Accounts and rows by judged role, kind, fit and target country.
active_within_daysRel3Keeps authors who posted recently.
questionsRel3Your own questions, judged on every row.
posts_per_accountRel3Posts walked per account.
include=authors, include=contacts, include=audienceRel3Author joins.
platforms=telegram with view=accountsRel3Telegram channels (on posts, platforms=telegram is limit L2).
search_in kind accountsRel3Account search on a platform.
label=stanceRel4The stance of each row.
comments=inline, comments=bulk, comments.top_posts, comments.per_post, comments.select_by, comments.min_length, comments.labelsRel4Comments on the top posts.
include=commentersRel4The authors of those comments.
brand_handlesRel4A brand's own and tagged posts and ads.
mentions_ofRel4Posts that mention an account.
review_sitesRel4Reviews of a brand across review sites.
search_in kinds owned, tagged, ads, reviews, and comments beyond RedditRel4Owned, tagged, ad, review and comment lanes.
platforms=xiaohongshu, platforms=douyinRel4Xiaohongshu and Douyin.
view=communities, view=itemsRel5Communities and trends.
mode=resolve, mode=board, seedsRel5Names resolved to accounts, and trend boards.
nearRel5Rows near a place.
authors_match=walkRel5Walking each listed author's posts.
include=interestRel5Search-interest series per group.
sites=auto, sites_auto_max, walk_urlsRel5Sites found for you, and pages walked per site.
search_in kinds listings, places, communities, and web discover_sites, walkRel5Listing, place, community and site-walk lanes.
include=archiveRel6Archived copies of images.
scheduleRel6Recurring runs.
webhookRel6Delivery to your URL when a run settles.
More than 20 groupsRel6Very large grids.

Next steps