Skip to content
Built for AI agentsConnect yours

Docs

AIMentionTracker API reference

Eleven read-only endpoints over what we have already collected. One header, one envelope, and a worked response for every one of them.

Start 7-day free trialStart with the quickstart

Card required, nothing charged for 7 days

Base URL and authentication

A key from app.aimentiontracker.ai/api-keys, shown once at creation. `Authorization: Bearer <key>` is accepted equivalently. Keys belong to the organisation, not to a person, so they survive that person leaving.

Every request looks like this
curl -H "X-API-Key: $AMT_API_KEY" https://app.aimentiontracker.ai/api/v1/me

# Authorization: Bearer is accepted equivalently
curl -H "Authorization: Bearer $AMT_API_KEY" https://app.aimentiontracker.ai/api/v1/me

300 requests a minute, the same on every plan. Errors and rate limits has the headers, the two different things that cause a 429, and every code you can be handed.

The response envelope

Success puts the result in data and the context you need to describe it in meta. meta.brand tells you which brand these numbers belong to, which is what stops a multi-brand client filing one customer's figures under another's.

Shape
{
  "data": ...,
  "meta": {
    "generated_at": "2026-09-30T09:00:00.000Z",
    "brand": { "id": "wsp_...", "name": "Acme" },
    "note": "..."
  }
}

Anywhere a rate appears it is this shape, never a bare number. The denominator travels with the figure so nobody has to guess how much is behind it:

Rate
{
  "present":    3,     // answers that named the brand
  "responses":  4,     // answers in the window — the denominator
  "rate":       null,  // present/responses to 3dp, or NULL below min_sample
  "min_sample": 5     // the floor below which rate is null
}

That is the case worth staring at. Three of four answers named the brand, and rate is null because four answers is too few to quote a percentage from. Rendering it as 0% reports a collapse in visibility that did not happen.

The eleven endpoints

All GET, all read-only, all returning the same envelope. Every table below is generated from the OpenAPI document, so it describes what the API actually returns rather than what someone remembered it returning.

GET/me

Who this key is, and the rules for reading the numbers

Call this first. The reporting_rules block is not advisory: it carries the sample floor and the live alert thresholds, which is the only way a client can tell 'no alerts' from 'nothing changed'.

Fields of data. (● = always present)
FieldTypeMeaning
organizationAlways presentobject—
organization.namestring | nullOrganisation name.
planAlways presentobject—
plan.namestringtrial | solo | starter | growth | agency.
plan.statusstringStripe subscription status.
plan.trial_ends_atstring | nullISO timestamp, null when not trialling.
plan.promptsobject—
plan.prompts.usedintegerActive prompts across the organisation.
plan.prompts.includedintegerThe plan's cap.
plan.brandsintegerBrands in the organisation.
keyAlways presentobject—
key.scopesstring[]Granted to this key.
key.available_scopesstring[]Every scope that exists, so 'not granted' is distinguishable from 'not real'.
key.pinned_to_brandbooleanTrue when the key is bound to one brand. Such a key refuses any other ?brand=, and GET /brands returns only that one brand.
brandAlways presentobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
rate_limitAlways presentobject—
rate_limit.per_minuteintegerRequests a minute. 300 on every plan.
rate_limit.remainingintegerLeft in the current minute.
rate_limit.resets_in_secondsintegerUntil the window rolls.
reporting_rulesAlways presentobjectNOT ADVISORY. A client that ignores these produces wrong reports.
reporting_rules.min_sampleintegerBelow this many answers, rate is null.
reporting_rules.rate_null_meansstringPlain-language restatement, for surfacing to an end user.
reporting_rules.engines_averagedbooleanAlways false. Engines disagree; a mean describes no real surface.
reporting_rules.alertsobject—
reporting_rules.alerts.enabledboolean—
reporting_rules.alerts.kindsstring[] | stringArray of requested kinds, or the string 'all' when none are set. Kinds: visibility_drop, visibility_rise, competitor_overtook, cited_not_named, negative_framing, engine_blackout.
reporting_rules.alerts.min_delta_pointsintegerPercentage points of change required to raise a finding.
reporting_rules.alerts.min_sample_each_sideintegerAnswers required on BOTH sides of the comparison.
reporting_rules.alerts.also_suppressed_below_statistical_noisebooleanAlways true. Even a move clearing min_delta_points is suppressed if it sits inside a 95% CI on the difference of two proportions. Whichever bar is HIGHER wins.
reporting_rules.alerts.empty_list_meansstringThe honest phrasing for an empty /alerts response.
Fields of meta. (● = always present)
FieldTypeMeaning
generated_atstring—
brandobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
notestring—
GET /me — response
{
  "data": {
    "organization": {
      "name": "Acme Inc"
    },
    "plan": {
      "name": "growth",
      "status": "active",
      "trial_ends_at": null,
      "prompts": {
        "used": 7,
        "included": 9
      },
      "brands": 2
    },
    "key": {
      "scopes": [
        "read"
      ],
      "available_scopes": [
        "read",
        "prompts:write",
        "brands:write"
      ],
      "pinned_to_brand": false
    },
    "brand": {
      "id": "wsp_zD6cssxGlJyF",
      "name": "Acme"
    },
    "rate_limit": {
      "per_minute": 300,
      "remaining": 299,
      "resets_in_seconds": 41
    },
    "reporting_rules": {
      "min_sample": 5,
      "rate_null_means": "not enough answers yet — NOT zero",
      "engines_averaged": false,
      "alerts": {
        "enabled": true,
        "kinds": "all",
        "min_delta_points": 15,
        "min_sample_each_side": 10,
        "also_suppressed_below_statistical_noise": true,
        "empty_list_means": "no change large enough to distinguish from noise at this sample size"
      }
    }
  },
  "meta": {
    "generated_at": "2026-09-30T09:00:00.000Z"
  }
}

Other statuses: 400, 401, 402, 403, 404, 429. See errors and rate limits.

GET/brands

Every brand this key may read

The ids to pass as ?brand=. A pinned key sees only its own.

data is an array. Each row: (● = always present)
FieldTypeMeaning
idAlways presentstringThe workspace public id. Pass as ?brand=.
nameAlways presentstringBrand name.
domainstring | nullThe brand's own domain, null before setup finishes.
configuredAlways presentbooleanFalse means setup was never finished. Data endpoints return 404 brand_not_configured for one of these rather than a page of zeroes, which would read as measured absence.
active_promptsAlways presentintegerPrompts currently running for this brand.
marketobject—
market.location_codeinteger | nullDataForSEO location code.
market.language_codestring | nullISO language code.
last_collected_atstring | nullISO timestamp of the most recent stored answer.
Fields of meta. (● = always present)
FieldTypeMeaning
generated_atstring—
brandobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
notestring—
pinnedbooleanTrue when this key is bound to one brand.
GET /brands — response
{
  "data": [
    {
      "id": "wsp_zD6cssxGlJyF",
      "name": "Acme",
      "domain": "acme.com",
      "configured": true,
      "active_prompts": 5,
      "market": {
        "location_code": 2840,
        "language_code": "en"
      },
      "last_collected_at": "2026-09-30T04:12:00.000Z"
    },
    {
      "id": "wsp_094oxa5e",
      "name": "Acme Labs",
      "domain": null,
      "configured": false,
      "active_prompts": 0,
      "market": {
        "location_code": 2840,
        "language_code": "en"
      },
      "last_collected_at": null
    }
  ],
  "meta": {
    "generated_at": "2026-09-30T09:00:00.000Z",
    "pinned": false,
    "note": "Pass one of these ids as ?brand= on every other endpoint."
  }
}

Other statuses: 400, 401, 402, 403, 404, 429. See errors and rate limits.

GET/visibility

Presence per engine and per brand over a trailing window

There is no single visibility score and there will not be one. The engines disagree, so an average describes no real surface; compute one yourself if you want it, knowing exactly what went in.

Query parameters
NameTypeNotes
daysinteger1 to 365, default 7
brandstringWhich brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other.
Fields of data. (● = always present)
FieldTypeMeaning
engines[]object[]—
engines[].engineAlways presentstringStable slug, e.g. chatgpt. Use this for ?engine= filters.
engines[].nameAlways presentstringDisplay name, e.g. ChatGPT.
engines[].collection_methodAlways presentstringHow this surface is read: api, scraper or serp.
engines[].presentAlways presentintegerAnswers that named this brand.
engines[].responsesAlways presentintegerAnswers in the window. The denominator, always returned.
engines[].rateAlways presentnumber | nullpresent/responses to 3dp, or NULL below min_sample (5). Null means NOT ENOUGH ANSWERS YET and must never be rendered as 0% — doing so reports a collapse in visibility that did not happen.
engines[].min_sampleAlways presentintegerThe floor below which rate is null.
engines[].citationsintegerAnswers from this engine that cited the brand's own domain.
engines[].avg_rank_when_namednumber | nullMean position in the list WHEN NAMED. Null when never named.
brands[]object[]—
brands[].nameAlways presentstringBrand name.
brands[].roleAlways present"self" | "competitor"Yours, or a rival.
brands[].presentAlways presentintegerAnswers that named this brand.
brands[].responsesAlways presentintegerAnswers in the window. The denominator, always returned.
brands[].rateAlways presentnumber | nullpresent/responses to 3dp, or NULL below min_sample (5). Null means NOT ENOUGH ANSWERS YET and must never be rendered as 0% — doing so reports a collapse in visibility that did not happen.
brands[].min_sampleAlways presentintegerThe floor below which rate is null.
brands[].citationsintegerAnswers citing this brand's own domain.
brands[].avg_rank_when_namednumber | nullMean list position when named.
brands[].framingobjectHow the brand was FRAMED where it appeared. Deliberately not called a sentiment score: it counts cue phrases, it does not grade tone.
brands[].framing.classifiedintegerMentions we could classify at all. The denominator here.
brands[].framing.recommendedintegerMentions framed as a recommendation.
brands[].framing.negativeintegerMentions framed negatively.
Fields of meta. (● = always present)
FieldTypeMeaning
generated_atstring—
brandobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
notestring—
window_daysinteger—
GET /visibility — response
{
  "data": {
    "engines": [
      {
        "engine": "chatgpt",
        "name": "ChatGPT",
        "collection_method": "scraper",
        "present": 18,
        "responses": 30,
        "rate": 0.6,
        "min_sample": 5,
        "citations": 7,
        "avg_rank_when_named": 2.4
      },
      {
        "engine": "perplexity",
        "name": "Perplexity",
        "collection_method": "api",
        "present": 3,
        "responses": 4,
        "rate": null,
        "min_sample": 5,
        "citations": 1,
        "avg_rank_when_named": 3
      }
    ],
    "brands": [
      {
        "name": "Acme",
        "role": "self",
        "present": 21,
        "responses": 34,
        "rate": 0.618,
        "min_sample": 5,
        "citations": 8,
        "avg_rank_when_named": 2.5,
        "framing": {
          "classified": 21,
          "recommended": 14,
          "negative": 1
        }
      }
    ]
  },
  "meta": {
    "generated_at": "2026-09-30T09:00:00.000Z",
    "brand": {
      "id": "wsp_zD6cssxGlJyF",
      "name": "Acme"
    },
    "window_days": 30,
    "note": "Engines are reported separately and never averaged. The perplexity row above has rate: null because 4 answers is below min_sample — that means NOT ENOUGH DATA, not 0%."
  }
}

Other statuses: 400, 401, 402, 403, 404, 429. See errors and rate limits.

GET/answers

The stored answers, with their mentions and citations

The raw material every other number traces back to. Keyset pagination: pass meta.next_cursor as ?cursor=. outcome='empty' is a real observation (the engine answered and named nobody), not a failure.

Query parameters
NameTypeNotes
limitinteger1 to 200, default 50
cursorstringAn answer id from a previous page.
enginestringFilter to one engine slug.
brandstringWhich brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other.
data is an array. Each row: (● = always present)
FieldTypeMeaning
idAlways presentstringOpaque public id, ans_<12>. Also the pagination cursor. Never an integer.
engineAlways presentstringEngine slug.
collection_methodstringapi, scraper or serp.
promptAlways presentstringThe prompt that was asked. Called `prompt`, not `question`.
answerstring | nullThe answer verbatim, markdown. Null when the engine returned none.
outcomeAlways present"ok" | "empty"'empty' is a REAL OBSERVATION: the engine answered and named nobody. It is in the denominator everywhere. Failed collections (error, timeout) are excluded entirely and never appear here.
collected_atAlways presentstringISO timestamp.
location_codeinteger | nullThe market this was collected in.
mentions[]Always presentobject[]—
mentions[].brandAlways presentstringWhich tracked brand this refers to.
mentions[].roleAlways present"self" | "competitor"—
mentions[].mentionedAlways presentbooleanFalse rows exist: we looked and it was absent.
mentions[].rankinteger | nullPosition in the answer's list, 1-based. Null when unranked.
mentions[].countinteger | nullTimes named in this one answer.
mentions[].framingstring | nullrecommended | neutral | negative | null when genuinely ambivalent.
mentions[].framing_signalsstring[] | nullThe cue phrases behind `framing`, so a judgement can be checked.
mentions[].snippetstring | nullThe verbatim sentence naming the brand. This is the evidence.
citations[]Always presentobject[]—
citations[].urlAlways presentstringThe cited page.
citations[].domainAlways presentstringIts host.
citations[].rankinteger | nullPosition in the engine's source list. That order IS the rank.
citations[].titlestring | nullPage title where the engine supplied one.
Fields of meta. (● = always present)
FieldTypeMeaning
generated_atstring—
brandobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
notestring—
next_cursorstring | nullPass as ?cursor=. Null on the last page.
GET /answers — response
{
  "data": [
    {
      "id": "ans_SfwqC8iCWwte",
      "engine": "chatgpt",
      "collection_method": "scraper",
      "prompt": "best project management tools for agencies",
      "answer": "For agencies, the most commonly recommended options are…",
      "outcome": "ok",
      "collected_at": "2026-09-30T04:12:00.000Z",
      "location_code": 2840,
      "mentions": [
        {
          "brand": "Acme",
          "role": "self",
          "mentioned": true,
          "rank": 2,
          "count": 1,
          "framing": "recommended",
          "framing_signals": [
            "best for"
          ],
          "snippet": "Acme is best for agencies juggling many clients."
        },
        {
          "brand": "Rival",
          "role": "competitor",
          "mentioned": false,
          "rank": null,
          "count": 0,
          "framing": null,
          "framing_signals": null,
          "snippet": null
        }
      ],
      "citations": [
        {
          "url": "https://example.com/best-pm-tools",
          "domain": "example.com",
          "rank": 1,
          "title": "Best PM tools in 2026"
        }
      ]
    }
  ],
  "meta": {
    "generated_at": "2026-09-30T09:00:00.000Z",
    "brand": {
      "id": "wsp_zD6cssxGlJyF",
      "name": "Acme"
    },
    "next_cursor": "ans_vIUZWjBPDEm2",
    "note": "Pass next_cursor as ?cursor= for the following page."
  }
}

Other statuses: 400, 401, 402, 403, 404, 429. See errors and rate limits.

GET/sources

The domains the engines read before answering

answers_citing_you_without_naming_you is the actionable column: a source read on nine answers that named a competitor every time is a specific page to go and be on. Hosts are normalised, so www.example.com and example.com are one row.

Query parameters
NameTypeNotes
daysinteger1 to 365, default 30
brandstringWhich brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other.
data is an array. Each row: (● = always present)
FieldTypeMeaning
domainAlways presentstringNormalised host — www.example.com and example.com are one row.
answers_citing_itAlways presentintegerAnswers in the window that cited this domain.
answers_where_you_were_namedAlways presentintegerOf those, how many also named your brand.
answers_citing_you_without_naming_youAlways presentintegerThe actionable number. A domain read on nine answers that named a competitor every time is a specific page to go and be on.
Fields of meta. (● = always present)
FieldTypeMeaning
generated_atstring—
brandobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
notestring—
window_daysinteger—
limitinteger—
GET /sources — response
{
  "data": [
    {
      "domain": "g2.com",
      "answers_citing_it": 12,
      "answers_where_you_were_named": 3,
      "answers_citing_you_without_naming_you": 9
    },
    {
      "domain": "acme.com",
      "answers_citing_it": 5,
      "answers_where_you_were_named": 5,
      "answers_citing_you_without_naming_you": 0
    }
  ],
  "meta": {
    "generated_at": "2026-09-30T09:00:00.000Z",
    "brand": {
      "id": "wsp_zD6cssxGlJyF",
      "name": "Acme"
    },
    "window_days": 30,
    "limit": 200,
    "note": "g2.com was read on 12 answers and named you on 3. Those other 9 are the work."
  }
}

Other statuses: 400, 401, 402, 403, 404, 429. See errors and rate limits.

GET/competitors

Who the engines name instead of you

answers_without_you is the column that matters. 'Named in 48 answers' is a fact about a competitor; 'named in 47 answers that never mentioned you' is a fact about you, and it is the one you can act on. Quotes are OFF by default and capped at three per competitor. They are the evidence behind the number, and pulling them for twenty competitors on every call turns a report into a transcript.

Query parameters
NameTypeNotes
daysinteger1 to 365, default 30
quotes"1"Pass 1 to include up to three supporting sentences per competitor. Omitted entirely when not requested.
brandstringWhich brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other.
data is an array. Each row: (● = always present)
FieldTypeMeaning
competitorAlways presentstringThe competitor's name as tracked in this workspace.
domainAlways presentstring | nullTheir domain, when one is recorded.
answers_naming_themAlways presentintegerAnswers in the window that named them.
answers_without_youAlways presentintegerThe actionable column. Answers that named this competitor and never mentioned you. 'Named in 48 answers' is a fact about them; 'named in 47 answers that never mentioned you' is a fact about you.
best_list_positionAlways presentinteger | nullBest (lowest) position they reached. Null when no answer ranked them.
typical_list_positionAlways presentnumber | nullMedian position to 1dp. Null when no answer ranked them.
total_mentionsAlways presentintegerTimes named across those answers, which can exceed answers_naming_them.
enginesAlways presentstring[]Engine slugs that named them. Listed, never pooled into one figure.
quotes[]object[]Up to three supporting sentences, best-ranked first. PRESENT ONLY with ?quotes=1, and absent rather than empty otherwise.
quotes[].snippetAlways presentstringThe sentence the engine wrote, with its ** emphasis left intact.
quotes[].engineAlways presentstringEngine slug the sentence came from.
quotes[].questionAlways presentstringThe tracked prompt whose answer contained it.
quotes[].listRankAlways presentinteger | nullWhere the engine placed them; 1 is first. Null when it wrote no list.
quotes[].answerIdAlways presentstringThe answer this came from, as returned by /answers.
Fields of meta. (● = always present)
FieldTypeMeaning
generated_atstring—
brandobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
notestring—
window_daysinteger—
total_answersintegerAnswers in the window, all engines.
answers_naming_youinteger—
answers_without_youintegertotal_answers - answers_naming_you.
GET /competitors — response
{
  "data": [
    {
      "competitor": "Rival",
      "domain": "rival.com",
      "answers_naming_them": 48,
      "answers_without_you": 47,
      "best_list_position": 1,
      "typical_list_position": 2.5,
      "total_mentions": 61,
      "engines": [
        "chatgpt",
        "perplexity"
      ]
    },
    {
      "competitor": "Otherco",
      "domain": null,
      "answers_naming_them": 6,
      "answers_without_you": 4,
      "best_list_position": null,
      "typical_list_position": null,
      "total_mentions": 6,
      "engines": [
        "gemini"
      ]
    }
  ],
  "meta": {
    "generated_at": "2026-09-30T09:00:00.000Z",
    "brand": {
      "id": "wsp_zD6cssxGlJyF",
      "name": "Acme"
    },
    "window_days": 30,
    "total_answers": 60,
    "answers_naming_you": 8,
    "answers_without_you": 52,
    "note": "answers_without_you is the actionable column: answers that named this competitor and never mentioned you. Engines are listed, never pooled into one figure. Pass quotes=1 for up to three supporting sentences per competitor."
  }
}

Other statuses: 400, 401, 402, 403, 404, 429. See errors and rate limits.

GET/recommendations

What to do next, ranked, with the evidence

CHECK `displayable` BEFORE REPORTING ANYTHING. When it is false the list is empty because the corpus is too thin to rank honestly, NOT because there is nothing to do, and reporting that as a clean bill of health is the worst mistake available on this endpoint. Those two states call for opposite reactions. Ordered by `tier`, which is an editorial judgement about whose permission the work needs rather than a measurement: your own page outranks a marketplace because nobody has to agree to it. There is deliberately NO blended score. A caller who disagrees with the order can see exactly which judgement to argue with, which is not true of 0.734. Every row cites the pages behind it. There is no 'write more content' and no 'add schema markup': an item with nothing to point at is filtered out rather than softened into advice.

Query parameters
NameTypeNotes
daysinteger1 to 365, default 30
limitinteger1 to 50, default 12
brandstringWhich brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other.
data is an array. Each row: (● = always present)
FieldTypeMeaning
kindAlways present"own_page" | "listing" | "community" | "marketplace"What kind of WORK this is, not what kind of site it is. You submit to a listing, you participate in a community, you rewrite your own page this afternoon.
tierAlways presentintegerSort order, and an argument rather than a score: 1 your own page (nobody has to agree), 2 a listing you can submit to this week, 3 a community you must participate in, 4 a marketplace that is usually a procurement decision.
siteAlways presentstringThe registrable site. One publisher, one outreach contact.
pages[]Always presentobject[]The specific pages, because 'get listed on medium.com' is advice and naming the roundup is a task. Never empty.
pages[].urlAlways presentstring—
pages[].titleAlways presentstring | null—
answersAlways presentintegerAnswers in the window that cited this site.
answers_naming_youAlways presentintegerOf those, how many named you. Zero for every gap, by definition.
competitors_namedAlways presentintegerDistinct tracked competitors named in those answers.
enginesAlways presentstring[]Which engines read it. Listed, never pooled into one figure.
shareAlways presentnumberFraction of meta.total_answers this door accounts for. Meaningless without that denominator, which is why it is returned beside it.
Fields of meta. (● = always present)
FieldTypeMeaning
generated_atstring—
brandobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
notestring—
window_daysinteger—
total_answersintegerThe denominator for every share above.
displayablebooleanFalse when the corpus is too thin to rank anything honestly. An empty list with displayable false means NOT YET, never NOTHING TO DO.
GET /recommendations — response
{
  "data": [
    {
      "kind": "own_page",
      "tier": 1,
      "site": "yoursite.com",
      "pages": [
        {
          "url": "https://yoursite.com/pricing",
          "title": "Pricing"
        }
      ],
      "answers": 17,
      "answers_naming_you": 1,
      "competitors_named": 4,
      "engines": [
        "chatgpt",
        "perplexity"
      ],
      "share": 0.28
    },
    {
      "kind": "listing",
      "tier": 2,
      "site": "someroundup.com",
      "pages": [
        {
          "url": "https://someroundup.com/best-tools",
          "title": "The 12 Best Tools"
        }
      ],
      "answers": 9,
      "answers_naming_you": 0,
      "competitors_named": 6,
      "engines": [
        "gemini"
      ],
      "share": 0.15
    }
  ],
  "meta": {
    "generated_at": "2026-10-13T09:00:00.000Z",
    "brand": {
      "id": "wsp_zD6cssxGlJyF",
      "name": "Acme"
    },
    "window_days": 30,
    "total_answers": 60,
    "displayable": true,
    "note": "Ordered by tier, which is an editorial judgement about whose permission the work needs, not by volume: your own page outranks a marketplace because nobody has to agree to it. Engines are listed, never pooled. `share` is a fraction of total_answers, which is the only denominator it is comparable against. Every row cites the pages behind it; a recommendation with nothing to point at is filtered out rather than softened into advice."
  }
}

Other statuses: 400, 401, 402, 403, 404, 429. See errors and rate limits.

GET/questions

The questions this workspace runs against the engines

Call this before interpreting a disappointing visibility number. Everything else in this API is derived from these questions, so a low figure may mean the wrong question rather than poor visibility, and the two have opposite fixes. `web_search_volume` IS NULL WHEN UNCHECKED, not when it is zero. Read `volume_checked_at` before quoting demand: rendering an unchecked volume as 0 asserts nobody searches a question nobody has measured. The allowance in meta counts ACTIVE questions across the whole organisation, not per workspace, because a question costs the same vendor spend whichever brand it sits under.

Query parameters
NameTypeNotes
brandstringWhich brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other.
data is an array. Each row: (● = always present)
FieldTypeMeaning
idAlways presentstringPublic id.
textAlways presentstring—
categoryAlways presentstring | null—
sourceAlways presentstringHow the question got here, for example user or suggested.
activeAlways presentbooleanInactive questions stop being run but keep the answers they already produced.
answersAlways presentintegerAnswers this question has produced in total, all engines.
answers_naming_youAlways presentinteger—
web_search_volumeAlways presentinteger | nullNULL MEANS NOT CHECKED, not zero. Reporting an unchecked volume as 0 asserts nobody searches a question nobody has measured. Read volume_checked_at before quoting demand.
volume_checked_atAlways presentstring | nullWhen volume was last looked up. Null when it never has been.
Fields of meta. (● = always present)
FieldTypeMeaning
generated_atstring—
brandobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
notestring—
questions_allowedintegerThe plan's cap on active questions, organisation-wide.
questions_usedinteger—
questions_remainingintegerZero means another question cannot be added on this plan.
GET /questions — response
{
  "data": [
    {
      "id": "qst_7Hk2",
      "text": "best ai visibility tools",
      "category": "category",
      "source": "user",
      "active": true,
      "answers": 121,
      "answers_naming_you": 1,
      "web_search_volume": 390,
      "volume_checked_at": "2026-10-01T00:00:00.000Z"
    },
    {
      "id": "qst_9Lp4",
      "text": "how to track brand mentions in chatgpt",
      "category": null,
      "source": "suggested",
      "active": true,
      "answers": 60,
      "answers_naming_you": 0,
      "web_search_volume": null,
      "volume_checked_at": null
    }
  ],
  "meta": {
    "generated_at": "2026-10-13T09:00:00.000Z",
    "brand": {
      "id": "wsp_zD6cssxGlJyF",
      "name": "Acme"
    },
    "questions_allowed": 5,
    "questions_used": 2,
    "questions_remaining": 3,
    "note": "These are the questions every other measurement in this API is derived from, so a disappointing visibility figure may mean the wrong question rather than poor visibility. web_search_volume is null when it has not been checked, NOT when it is zero: check volume_checked_at before reporting demand. The allowance counts active questions across the whole organisation, because a question costs the same vendor spend whichever brand it sits under."
  }
}

Other statuses: 400, 401, 402, 403, 404, 429. See errors and rate limits.

GET/sentiment

How the engines frame each brand when they name it

NOT a score. THE DENOMINATOR IS `classified`, NOT `answers_naming_them`, and that is the whole shape of this endpoint. The classifier abstains on most listicle mentions by design: being item four in '12 best tools' is a mention with no opinion attached, and inventing one would be the product making something up. So `classified` is usually much smaller than `answers_naming_them`, and a share computed against the latter understates every brand's positivity by the abstention rate. There is no single sentiment number per brand, for the same reason there is no single visibility score: recommended and negative are separate counts over a shared denominator, and collapsing them discards the actionable part.

Query parameters
NameTypeNotes
daysinteger1 to 365, default 30
brandstringWhich brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other.
data is an array. Each row: (● = always present)
FieldTypeMeaning
brandAlways presentstringThe brand this row describes.
is_youAlways presentbooleanTrue for the workspace's own brand.
answers_naming_themAlways presentintegerAnswers that named this brand. NOT the denominator.
classifiedAlways presentintegerTHE DENOMINATOR. Mentions the classifier was willing to call. It abstains on listicle mentions, where an engine names a brand without endorsing it, so this is usually much smaller than answers_naming_them.
recommendedAlways presentintegerOf the classified mentions, how many read as a recommendation.
negativeAlways presentintegerOf the classified mentions, how many read as negative.
neutralAlways presentintegerclassified - recommended - negative, floored at 0.
recommended_shareAlways presentnumber | nullrecommended/classified to 3dp, or NULL below min_sample (5) classified mentions. Null means NOT ENOUGH CLASSIFIED MENTIONS and must never be rendered as 0%.
Fields of meta. (● = always present)
FieldTypeMeaning
generated_atstring—
brandobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
notestring—
window_daysinteger—
min_sampleintegerClassified-mention floor below which recommended_share is null.
GET /sentiment — response
{
  "data": [
    {
      "brand": "Acme",
      "is_you": true,
      "answers_naming_them": 21,
      "classified": 9,
      "recommended": 6,
      "negative": 1,
      "neutral": 2,
      "recommended_share": 0.667
    },
    {
      "brand": "Rival",
      "is_you": false,
      "answers_naming_them": 48,
      "classified": 3,
      "recommended": 2,
      "negative": 0,
      "neutral": 1,
      "recommended_share": null
    }
  ],
  "meta": {
    "generated_at": "2026-09-30T09:00:00.000Z",
    "brand": {
      "id": "wsp_zD6cssxGlJyF",
      "name": "Acme"
    },
    "window_days": 30,
    "min_sample": 5,
    "note": "classified is the denominator, not answers_naming_them: the classifier abstains on listicle mentions where the engine named a brand without endorsing it. recommended_share is null below min_sample, which means NOT ENOUGH CLASSIFIED MENTIONS, never zero."
  }
}

Other statuses: 400, 401, 402, 403, 404, 429. See errors and rate limits.

GET/share-of-voice

Each tracked brand's mentions as a share of every tracked brand's

THE ONE PLACE THIS API COMBINES A COMPETITOR'S NUMBER WITH YOURS. Everywhere else engines are reported separately and nothing is averaged; share of voice is a ratio by definition, so the honest move is to publish the denominator beside it rather than refuse the metric. THE DENOMINATOR IS THE BRANDS YOU TRACK, NOT THE MARKET. A workspace tracking two competitors and one tracking twelve produce incomparable numbers from identical answers, so always report brands_counted alongside.

Query parameters
NameTypeNotes
daysinteger1 to 365, default 30
brandstringWhich brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other.
data is an array. Each row: (● = always present)
FieldTypeMeaning
brandAlways presentstringThe brand this row describes.
is_youAlways presentbooleanTrue for the workspace's own brand.
mentionsAlways presentintegerAnswers in the window that named this brand.
shareAlways presentnumber | nullThis brand's mentions over every tracked brand's, to 4dp. NULL when no brand was named at all, because a share of an empty pool is undefined. That is not the same as a 0% share.
Fields of meta. (● = always present)
FieldTypeMeaning
generated_atstring—
brandobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
notestring—
window_daysinteger—
total_mentionsintegerThe denominator. Sum of every tracked brand's mentions.
brands_countedintegerHow many brands went into the denominator.
GET /share-of-voice — response
{
  "data": [
    {
      "brand": "Rival",
      "is_you": false,
      "mentions": 48,
      "share": 0.6957
    },
    {
      "brand": "Acme",
      "is_you": true,
      "mentions": 21,
      "share": 0.3043
    }
  ],
  "meta": {
    "generated_at": "2026-09-30T09:00:00.000Z",
    "brand": {
      "id": "wsp_zD6cssxGlJyF",
      "name": "Acme"
    },
    "window_days": 30,
    "total_mentions": 69,
    "brands_counted": 2,
    "note": "The denominator is the brands THIS workspace tracks, not the market. Two workspaces tracking different competitor sets produce incomparable shares from the same answers, so always report brands_counted alongside. share is null when no brand was named at all, which is not the same as a 0% share."
  }
}

Other statuses: 400, 401, 402, 403, 404, 429. See errors and rate limits.

GET/alerts

Findings about your brand, newest first

AN EMPTY LIST DOES NOT MEAN NOTHING CHANGED. A finding is raised only when both comparison windows carry enough answers AND the change is larger than its own margin of error. Read an empty list as 'no change large enough to distinguish from noise at this sample size'. GET /me returns the thresholds.

Query parameters
NameTypeNotes
limitinteger1 to 200, default 50
brandstringWhich brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other.
data is an array. Each row: (● = always present)
FieldTypeMeaning
idAlways presentstringOpaque public id, alr_<12>. Use it to dedupe across polls.
kindAlways presentstringFinding type, e.g. visibility_drop.
titleAlways presentstringOne line.
detailstringThe explanation, including the numbers behind it.
factsobjectThe figures the finding was computed from.
created_atAlways presentstringISO timestamp.
readAlways presentbooleanWhether it has been seen in the dashboard.
Fields of meta. (● = always present)
FieldTypeMeaning
generated_atstring—
brandobjectWhich brand a response describes. Present in meta on every data endpoint.
brand.idAlways presentstringPass as ?brand=.
brand.nameAlways presentstringDisplay name.
notestring—
GET /alerts — response
{
  "data": [],
  "meta": {
    "generated_at": "2026-09-30T09:00:00.000Z",
    "brand": {
      "id": "wsp_zD6cssxGlJyF",
      "name": "Acme"
    },
    "note": "An empty list does NOT mean nothing changed. It means no move was large enough to distinguish from noise at this sample size."
  }
}

Other statuses: 400, 401, 402, 403, 404, 429. See errors and rate limits.

Rules that govern every response

Three rules you must not break

These are not style preferences. Breaking them produces a confidently wrong report in your customer's name.

  1. 1

    A null rate means "not enough answers yet". It is NOT zero.

    Rates come back as {present, responses, rate, min_sample}. When rate is null, the sample is below the floor. Say “3 of 4 answers, too few to quote a percentage”. Never render it as 0%, and never let it into an average. Reporting “0% visibility” for a brand that was named in 3 of 4 answers is the single most damaging thing you can do with this data.

  2. 2

    Never average the engines.

    There is no single visibility score and you must not compute one. ChatGPT, Perplexity and Google AI Overviews are different surfaces that disagree, so a mean describes nothing real. Report per engine. If the user insists on one number, give them the pooled counts (present and responses summed) and say plainly what you did.

  3. 3

    An empty alerts list does NOT mean nothing changed.

    A finding is raised only when both comparison windows carry enough answers AND the move is larger than its own margin of error. On a small plan that excludes most real movement. Say “no change large enough to distinguish from noise at this sample size”. GET /me shows the thresholds in force.

The long version, including why a failed collection on our side never counts as your brand being absent, is on how to read our numbers. The reasoning behind all of it is on our public methodology page.

API reference questions

Answered from the implementation, not from a brief.

What does the AIMentionTracker API return?
Every response is an envelope. data carries the result; meta carries generated_at, the brand the numbers belong to, and often a note explaining a subtlety of that endpoint — the window in days, the next cursor, or whether the key is pinned. An error replaces data with an error object holding a stable code and a human message.
Does the AIMentionTracker API have write endpoints?
No, and not by accident. The two obvious writes both spend money with no human present: adding a prompt expands a metered bill, and triggering a collection run spends vendor credit. Both stay in the app behind a confirmation. All eleven endpoints are GET.
How does pagination work in the AIMentionTracker API?
Only /answers paginates. Pass limit up to 200, then pass meta.next_cursor back as ?cursor= to continue. The cursor is the opaque id of the last answer on the page. A cursor we did not issue, or one belonging to another brand, returns 400 bad_cursor rather than silently restarting at page one.
Why is rate sometimes null in the API response?
Because the sample is below the floor of 5 answers. Null means "not enough answers yet", never zero. The worked example on this page deliberately includes a Perplexity row at present 3, responses 4, rate null, because that is the case a client is most likely to render wrongly as 0%.
Can I get one overall AI visibility score from the API?
No, and you should not compute one. Engines retrieve differently and disagree, so a mean describes no surface that exists. If you genuinely need a single figure, sum present and sum responses across engines and say plainly that is what you did. GET /me returns engines_averaged: false so a client can state this rather than assume it.
Does the API count AIMentionTracker outages as my brand being absent?
No. A failed collection — an error or a timeout — is excluded from the denominator entirely and never appears in /answers. Separately, outcome "empty" is a real observation: the engine answered and named nobody. That one is counted, because it happened.
Is the AIMentionTracker API documented with OpenAPI?
Yes. There is an OpenAPI 3.1 document with a schema and a worked example for every path, and this page is generated from it. A test in the API's own suite compares the documented property names against what the handlers actually return, in both directions, so a renamed field fails a build rather than becoming a quietly wrong sentence here.

7-day trial

The API reads what AIMentionTracker collects

AIMentionTracker tracks your AI mentions and helps you get recommended by ChatGPT, Claude and Google AI Overviews. Add your brand and the prompts your buyers type, and the first answers are readable in full within minutes.

Card required, nothing charged for 7 days.

On every plan

  • Every answer stored in full, with its citations.
  • Every rate shown with the number of checks behind it.
  • Cited-but-not-named reported as its own state.