# Discover (/docs/prism/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. ```bash title="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" ``` ```json title="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. ```bash title="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`](/docs/errors.md#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: | Status | Meaning | What to do | | --- | --- | --- | | `queued` | Waiting: your account already runs two background runs. | Wait on `next.status_url`. | | `running` | Collecting, judging or delivering. | Wait on `next.status_url`; read rows as they arrive. | | `paused` | Stopped 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. | | `partial` | Finished 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. | | `completed` | Finished. | Read every kept row at `next.results_url`, free. | | `failed` | Nothing could be collected. | Retry later, or drop the failing platforms. | | `cancelled` | Stopped 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. | Call | What it does | | --- | --- | | `GET /v1/prism/discover/runs` | Your 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=25` | Status, 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.jsonl` | Every 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}/legs` | Every 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}/resume` | Continues 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.=`, `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..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 | Field | Default | Meaning | | --- | --- | --- | | `keywords` | none | The searches, comma separated; each runs once on each platform. Up to 6 run in one call. | | `query` | none | One search phrase, never split on commas. | | `context` | the keywords | What kept rows must be about, 3 to 2,000 characters. A persona or a question goes here, never in `keywords`. | | `brands` (alias `brand`) | none | Each brand is its own group, searched by its name and labelled with the `mention` judgment. | | `platforms` or `preset` | `preset=consumer` | The platforms, in order, or a named preset. | | `period`, or `since` and `until` (aliases `from`, `to`) | none | `Nd` (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). | | `view` | `posts` | What the run returns. | | `limit` | 50 on the GET (quick 20), 500 on the POST | Kept 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_credits` | 50 on the GET | The most the run may spend. Required on the POST unless `dry_run` is true. | | `depth` | `standard` | `quick` (at most 3 fast platforms, 15 seconds) or `deep` (background). | | `dry_run` | `false` | The plan and the estimate, free. | ### Tier 2 | Field | Meaning | | --- | --- | | `country`, `exclude_country`, `unknown_country` | Where 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`. | | `languages` | Rows in these languages; searches are planned in them. | | `min_views`, `min_likes`, `min_comments`, `min_followers`, `max_followers`, `verified`, `media_type`, `link_domains` | Row and author filters, applied by code before anything is judged. `media_type` is `video`, `image`, `carousel` or `text`. | | `exclude_terms`, `contains` | Drop 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_length` | How undated rows, estimated dates, rows outside the window and very short rows are handled. | | `sites` | Domains 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_threshold` | `filter` (default), `score` or `off`; the threshold is 0.5 by default. | | `label`, `intent` | Judgments 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. | | `sponsored` | `keep` (default), `exclude` or `only` rows flagged or disclosed as sponsored. | | `limit_per_keyword`, `sort`, `sample`, `max_per_author`, `max_per_platform` | How many rows per keyword, in which order (`relevance`, `recent`, `engagement`, `views` or `views_per_follower`), and a representative sample. | | `include` | `author_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`, `fields` | `evidence` (default), `compact` or `text`; `fields` keeps only the keys you name. | | `author_identity` | `full` (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_to` | Adds `text_translated` in this language. | | `continue`, `since_watermark` | The token from `data.next.continue_url`; a watermark from an earlier run returns only newer rows. | ### Tier 3 | Field | Meaning | | --- | --- | | `groups` | POST 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_match` | Keep only rows by these authors. | | `hashtags`, `search_in` | Hashtag searches; lane kinds per platform (Reddit comments, Naver's corpora, web sites). | | `expand` | 0 (default) to 3 extra searches planned per keyword. | | `pages_per_platform` | 1 (default) to 10; above 3 the run continues in the background. | | `retention_days` | 1 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`, `async` | The GET's time budget (10 to 96 seconds), and whether it may continue in the background. | | `fresh`, `new_run`, `exclude` | Skip cached search pages at full price, start a new run, and leave out ids you already hold. | | `.` | A platform search's own parameter, such as `tiktok.region` or `instagram.surface=relevance`. | | `cursor`, `page_size` | GET 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 | # | Request | Available | | --- | --- | --- | | 1 | Brand reaction for a month: `brands=aruba,curacao,bonaire&preset=travel&period=last_month&tz=America/New_York&exclude_terms=networks&max_credits=150` | Today | | 2 | A chat agent's persona question: `query=electric cars&context=What do women over 50 say about owning an electric car?&depth=quick&period=90d` | Today | | 3 | Sellers behind a topic: `view=accounts&role=seller,reseller&targets_country=AE` | Rel3 | | 4 | Creators in a country: `view=accounts&preset=creators&country=ES&min_followers=50000&include=contacts` | Rel3 | | 5 | Trends with no keyword: `view=items&country=DE&period=14d` | Rel5 | | 6 | Communities: `view=communities&platforms=reddit,facebook,telegram` | Rel5 | | 7 | Links 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 run | Today | | 8 | Market research for a client: a POST with one group per topic and market, tagged, with `include=dropped` and `body` | Today, in the [research guide](/docs/prism/discover-research.md) | 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 | Preset | Platforms, in order | Use it when | | --- | --- | --- | | `consumer` | youtube, reddit (posts and comments), tiktok, twitter, threads, news | What people say about a topic, product or brand (the default). | | `b2b` | linkedin, reddit, news, web, twitter, hackernews | Business and professional topics. | | `travel` | instagram, tiktok, youtube, reddit, pinterest, news | Destinations, hotels and trips. | | `apps` | reddit, tiktok, youtube, twitter, news | Reactions to an app or a software product. | | `news` | news, web, reddit, twitter, hackernews | Coverage and discussion of an event or a company. | | `commerce` | tiktok, instagram, youtube, reddit, pinterest | Products and shopping. | | `creators` | tiktok, instagram, youtube | Short-video and creator platforms only. | | `korea` | naver, youtube, tiktok, instagram, threads, news, web | Korean-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..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..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). | Platform | Price per search page | How the window is applied | Reach | | --- | --- | --- | --- | | TikTok | 1 credit | The 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. | | Instagram | 1 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. | | YouTube | 1 credit | Exact dates. | Any window. | | Reddit | 1 credit | The narrowest time bucket that misses at most 2 days, with pages. | Any window, with pages. | | X | 1 credit | Exact dates, newest first. | Any window, with pages. | | Facebook, Threads | 1 credit | Exact dates. | Any window. | | LinkedIn | 5 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. | | Pinterest | 1 credit | Pages filtered locally. | Rows of any age arrive in relevance order, so pages add in-window rows at a steady low rate. | | Hacker News, news | 1 credit | Exact dates. | Any window; news returns at most 100 rows a call. | | Bluesky, GitHub | 1 credit | Pages filtered locally. | Rows of any date the search returns; only dated rows count in the window. | | Quora | 5 credits | Pages filtered locally. | Rows of any date the search returns; only dated rows count in the window. | | Forums | 10 credits | One fused search across forums, filtered locally. | Rows of any date the search returns; only dated rows count in the window. | | Web | 2 credits per 10 results | Exact 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, cafe | 1 credit | The 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 | Item | Price | | --- | --- | | A platform search | The 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 keywords | Free. 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 keywords | 1 credit per started 15 judged rows. | | Each `label` value | 1 credit per started 25 judged rows, each. | | A topic card (a `context` over 200 characters) | 1 credit. | | `translate_to` | 1 credit per started 1,000 translated characters. | | `sites` | Per 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=body` | The 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 run | Free. | 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](/docs/errors.md) with `details.reason` and `details.next_step`. `partial` and `paused` are not errors: they are 200 answers with `data.run`. | Code | Status | When | | --- | --- | --- | | `INVALID_REQUEST` | 400 | `invalid_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_EXCEEDED` | 402 | The balance or the key's credit limit is below what the run needs. | | [`RUN_NOT_FOUND`](/docs/errors.md#run-not-found) | 404 | No run with this id belongs to your account. | | [`RUN_STATE_CONFLICT`](/docs/errors.md#run-state-conflict) | 409 | The run's state does not allow the call, for example resuming a completed run. | | [`RUN_EXPIRED`](/docs/errors.md#run-expired) | 410 | A finished run's retention passed, or the run was purged. | | `PAYLOAD_TOO_LARGE` | 413 | The URL is too long (send a POST, or store long author lists), or a POST body is over 1 MB. | | [`NEEDS_ASYNC`](/docs/errors.md#needs-async) | 422 | Only 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_ERROR` | 502 | `all_legs_failed`: nothing was collected, and nothing was charged. | | `SERVICE_UNAVAILABLE` | 503 | `store_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 window | Applied to every row, with extra pages walked to reach a past window | `since` and `until` keep rows on every platform, on one page per platform | Ranks rows by date but does not filter them to a window | | Judging | Every row against your keywords or your question, and duplicates removed across platforms | Relevance against the query on every row; `relevance=filter` drops off-topic rows | Reranked and clustered | | Size | One call, or a background run of thousands of searches that you can resume | One page per platform, in one call | One call | | Price | Each platform's own page price | Each platform's own page price | Flat 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 | Id | Limit | Nearest route | | --- | --- | --- | | L1 | It 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. | | L2 | It 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. | | L3 | It cannot say where a Reddit user lives; Reddit publishes no country. | Community-level hints. | | L4 | X and Reddit have no per-country trends. | Rows from the pool and Reddit's own feeds. | | L5 | Instagram and TikTok have no platform-wide comment search. | Comments under posts found by search; Reddit comment search. | | L6 | It finds what each platform's own search returns, no more. | More keywords, hashtags and platforms. | | L7 | It has no platform-wide history of a sound or a hashtag. | Scheduled runs with watermarks. | | L8 | It 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. | | L9 | It does not cover TV or radio broadcasts. | News coverage of a broadcast. | | L10 | TikTok keyword search does not reach a window older than 180 days. | Walk the posts of the accounts it found. | | L11 | Content 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](https://www.geonames.org/) 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 value | Release | What it does | | --- | --- | --- | | `view=accounts`, `sort=fit`, `sort=followers` | Rel3 | The accounts behind a topic, and their order. | | `role`, `account_kind`, `targets_country`, `label=role`, `label=account_kind`, `label=fit` | Rel3 | Accounts and rows by judged role, kind, fit and target country. | | `active_within_days` | Rel3 | Keeps authors who posted recently. | | `questions` | Rel3 | Your own questions, judged on every row. | | `posts_per_account` | Rel3 | Posts walked per account. | | `include=authors`, `include=contacts`, `include=audience` | Rel3 | Author joins. | | `platforms=telegram` with `view=accounts` | Rel3 | Telegram channels (on posts, `platforms=telegram` is limit L2). | | `search_in` kind `accounts` | Rel3 | Account search on a platform. | | `label=stance` | Rel4 | The stance of each row. | | `comments=inline`, `comments=bulk`, `comments.top_posts`, `comments.per_post`, `comments.select_by`, `comments.min_length`, `comments.labels` | Rel4 | Comments on the top posts. | | `include=commenters` | Rel4 | The authors of those comments. | | `brand_handles` | Rel4 | A brand's own and tagged posts and ads. | | `mentions_of` | Rel4 | Posts that mention an account. | | `review_sites` | Rel4 | Reviews of a brand across review sites. | | `search_in` kinds `owned`, `tagged`, `ads`, `reviews`, and `comments` beyond Reddit | Rel4 | Owned, tagged, ad, review and comment lanes. | | `platforms=xiaohongshu`, `platforms=douyin` | Rel4 | Xiaohongshu and Douyin. | | `view=communities`, `view=items` | Rel5 | Communities and trends. | | `mode=resolve`, `mode=board`, `seeds` | Rel5 | Names resolved to accounts, and trend boards. | | `near` | Rel5 | Rows near a place. | | `authors_match=walk` | Rel5 | Walking each listed author's posts. | | `include=interest` | Rel5 | Search-interest series per group. | | `sites=auto`, `sites_auto_max`, `walk_urls` | Rel5 | Sites found for you, and pages walked per site. | | `search_in` kinds `listings`, `places`, `communities`, and web `discover_sites`, `walk` | Rel5 | Listing, place, community and site-walk lanes. | | `include=archive` | Rel6 | Archived copies of images. | | `schedule` | Rel6 | Recurring runs. | | `webhook` | Rel6 | Delivery to your URL when a run settles. | | More than 20 `groups` | Rel6 | Very large grids. | ## Next steps - [Discover for agents](/docs/prism/discover-agents.md)} description="The two agent tools, the retry policy and three worked transcripts." /> - [Market research with discover](/docs/prism/discover-research.md)} description="A topic and market grid in one run, with the drop log and the site access table." /> - [Errors](/docs/errors.md)} description="Every error code, with its fix." />