Skip to content
MentionBird.ai
Browse the documentation

Get Top Web Searches

The searches AI models ran before answering this brand's prompts - the terms to rank for, as opposed to the pages already cited.

GET /searches

Query parameters

Name Type Description
brand required string Tracked brand name, exactly as returned by listTrackedBrands.
start_date string ISO start date (YYYY-MM-DD). Defaults to 30 days ago.
end_date string ISO end date (YYYY-MM-DD). Defaults to today.
brand_prompt_id string Limit to one prompt — token from listQueries.
query_tag string Filter to one tag slug.
llm_provider string Filter to one AI platform.
One of: openai claude gemini perplexity grok
search string Substring match on the search text. Narrows the rows and total_occurrences; everything under coverage is run-level and stays put. Applied before grouping, so under a filter a topic's counts describe only its matching phrasings, and its label may re-spell or re-order to match them. Topic identity is mined nightly and does not move with the filter; the label's word set follows that identity, only its wording follows the filter.
group_by string
default topic
topic (default) returns the nightly-mined topic hierarchy flattened pre-order, each row carrying depth, topic_id and parent_topic_id; search returns one flat row per exact search string. Anything else is a 400, not a silent fallback - this selects which rows you get. Must match search_queries.VALID_GROUP_BY exactly. The older spelling theme is still accepted for topic and is deliberately not listed here, so nothing new depends on it.
One of: topic search
language string ISO 639-1 code selecting WHICH of the brand's topic trees to read. A brand running prompts in several languages has one tree per language, mined with that language's stemmer, so they cannot be merged and there is no all-languages view: this narrows the rows as well as the grouping. Omit for the brand's largest language. Not an enum and not a 400 - which languages exist is per-brand, so an unknown code falls back to that default and the response's language field says which was used. available_languages lists every one with its own search count; those counts sum to the brand's whole volume, so nothing is hidden by there being no combined view. An empty string means the searches are not stemmed - what an untagged or unsupported language gets; pass und to select that bucket, since its code is the empty string and an empty query parameter reads as an absent one. An unrecognised code names nothing and falls back to the default rather than landing on the unstemmed bucket. Most brands have one language and an empty available_languages.
limit integer
default 20
Max rows (default 20, max 100).
offset integer
default 0
Rows to skip — the cursor for reading past `limit`. Default 0. While `has_more` is true, repeat with offset advanced by `limit`.

Example request

curl -H "Authorization: Bearer $MENTIONBIRD_API_KEY" \
  "https://app.mentionbird.ai/api/v1/searches?brand=Acme"

Example response

Recorded from a live call against a demo workspace, so the shape is exactly what the endpoint returns.

{
  "available_languages": [],
  "coverage": {
    "by_provider": [
      {
        "exposes_searches": true,
        "no_search_rate_pct": 0.0,
        "provider": "openai",
        "provider_display": "ChatGPT",
        "runs": 1,
        "runs_with_readable_searches": 1,
        "runs_without_search": 0,
        "searches": 1
      }
    ],
    "no_search_rate_pct": 0.0,
    "runs": 1,
    "runs_with_readable_searches": 1,
    "runs_without_search": 0
  },
  "date_range": {
    "days": 30,
    "end": "2026-09-24",
    "start": "2026-08-26"
  },
  "filters_applied": {
    "brand": "Acme",
    "brand_prompt_id": null,
    "end_date": "2026-09-24",
    "group_by": "topic",
    "language": "",
    "llm_provider": null,
    "offset": 0,
    "query_tag": null,
    "search": null,
    "start_date": "2026-08-26"
  },
  "has_more": false,
  "items": [
    {
      "depth": 0,
      "direct_occurrences": 1,
      "first_seen_in_range": "2026-09-24",
      "last_searched": "2026-09-24",
      "occurrences": 1,
      "parent_topic_id": null,
      "pct_of_searches": 100.0,
      "prompt_count": 1,
      "search": "Other searches",
      "searches_by_provider": {
        "openai": 1
      },
      "sub_topics": 0,
      "topic_id": 1,
      "variants": 1
    }
  ],
  "language": "",
  "returned_count": 1,
  "searches_by_provider_total": {
    "openai": 1
  },
  "topics_ready": true,
  "total_count": 1,
  "total_occurrences": 1,
  "total_unique_searches": 1,
  "unmatched_pct_of_searches": 100.0,
  "unmatched_terms": 1
}

Responses

200
Searches ranked by how often they were run. Read the coverage block before quoting any rate: a provider with exposes_searches false does not publish its searches, so its answers are absent rather than zero. no_search_rate_pct is the share of readable answers where the model searched nothing and replied from prior knowledge - it is computed over runs_with_readable_searches, never over runs, and is null when nothing was readable. Search rankings cannot reach the prompts in that share. In topic mode rows come back pre-order - a topic, then its sub-topics - with depth, topic_id and parent_topic_id for rebuilding the nesting. In topic mode the search field is a synthesized label naming what the topic is about, not a quotation: it will not appear verbatim under group_by=search, and must not be fed back as the q filter or re-run as a search string. occurrences on a topic INCLUDES its sub-topics, so those numbers do not sum to total_occurrences; direct_occurrences is the row's own share and those do sum exactly. The last row is Other searches, the terminal bucket for searches too rare to have been mined into a topic and the one label that names no subject; unmatched_terms / unmatched_pct_of_searches report its size, which is a large share of volume - the visible rows are not the whole picture. topics_ready false means the brand's tree has not been mined yet and rows are one topic per search term. total_occurrences and total_unique_searches are identical in both modes and neither is the pagination total - walk on total_count, which counts topic rows here and search strings under group_by=search. Every one of these counts is zero for a provider with exposes_searches false because there is nothing to count, which is not the same as the model having searched nothing. language names the tree these rows came from and available_languages every tree the brand has; for a multi-language brand these rows are one language's slice, not the brand's whole volume. Each row carries searches_by_provider, a provider code to count map summing to occurrences, and the response carries searches_by_provider_total, the same map over every row in scope. Do NOT read a row's raw percentages as a model preference: they conflate how many runs were bought on each model with how much each model searches per run, which on real data turned a 1.7x preference into an apparent 15x one. Divide by searches_by_provider_total to get the topic's share of that provider's own searching, which cancels both. A provider that publishes nothing is absent from both maps rather than present at zero, so a missing key means unknown, not zero searches. searches_by_provider_total is row-level and so is narrowed by search, unlike coverage.by_provider[*].searches which reads the pre-filter run set; with no filters applied the two agree. Every row carries first_seen_in_range, the earliest run WITHIN the requested window rather than the first time the term was ever searched - on a 30-day read every value is bounded below by start_date, so a term the brand has triggered for a year reads as new; widen the window to push the bound back, there is no never-seen-before flag. Under group_by=search each row also carries triggered_by: the prompt that produced that search most often, as brand_prompt_id (the same opaque token listQueries returns, passable straight back as the brand_prompt_id filter), prompt and other_prompts. It names one prompt rather than listing them because 94.6% of terms come from exactly one, and is null only when that prompt has since been removed. Topic rows carry prompt_count instead, for the same reason depth and topic_id appear only on topics: a topic spans several prompts, so naming one would be a lie of omission.
400
Validation error — check the message in `error.message`.
401
Unauthorized — missing, invalid or revoked bearer token.
403
Forbidden — plan lacks API access, or key is read-only.
404
Not found — the brand, prompt or resource does not exist.
405
Method not allowed.
500
Internal error — logged; retry is safe for reads.