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.
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.
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/me300 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.
{
"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:
{
"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 /meWho this key is, and the rules for reading the numbers
- GET /brandsEvery brand this key may read
- GET /visibilityPresence per engine and per brand over a trailing window
- GET /answersThe stored answers, with their mentions and citations
- GET /sourcesThe domains the engines read before answering
- GET /competitorsWho the engines name instead of you
- GET /recommendationsWhat to do next, ranked, with the evidence
- GET /questionsThe questions this workspace runs against the engines
- GET /sentimentHow the engines frame each brand when they name it
- GET /share-of-voiceEach tracked brand's mentions as a share of every tracked brand's
- GET /alertsFindings about your brand, newest first
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'.
| Field | Type | Meaning |
|---|---|---|
| organizationAlways present | object | — |
| organization.name | string | null | Organisation name. |
| planAlways present | object | — |
| plan.name | string | trial | solo | starter | growth | agency. |
| plan.status | string | Stripe subscription status. |
| plan.trial_ends_at | string | null | ISO timestamp, null when not trialling. |
| plan.prompts | object | — |
| plan.prompts.used | integer | Active prompts across the organisation. |
| plan.prompts.included | integer | The plan's cap. |
| plan.brands | integer | Brands in the organisation. |
| keyAlways present | object | — |
| key.scopes | string[] | Granted to this key. |
| key.available_scopes | string[] | Every scope that exists, so 'not granted' is distinguishable from 'not real'. |
| key.pinned_to_brand | boolean | True when the key is bound to one brand. Such a key refuses any other ?brand=, and GET /brands returns only that one brand. |
| brandAlways present | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.idAlways present | string | Pass as ?brand=. |
| brand.nameAlways present | string | Display name. |
| rate_limitAlways present | object | — |
| rate_limit.per_minute | integer | Requests a minute. 300 on every plan. |
| rate_limit.remaining | integer | Left in the current minute. |
| rate_limit.resets_in_seconds | integer | Until the window rolls. |
| reporting_rulesAlways present | object | NOT ADVISORY. A client that ignores these produces wrong reports. |
| reporting_rules.min_sample | integer | Below this many answers, rate is null. |
| reporting_rules.rate_null_means | string | Plain-language restatement, for surfacing to an end user. |
| reporting_rules.engines_averaged | boolean | Always false. Engines disagree; a mean describes no real surface. |
| reporting_rules.alerts | object | — |
| reporting_rules.alerts.enabled | boolean | — |
| reporting_rules.alerts.kinds | string[] | string | Array 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_points | integer | Percentage points of change required to raise a finding. |
| reporting_rules.alerts.min_sample_each_side | integer | Answers required on BOTH sides of the comparison. |
| reporting_rules.alerts.also_suppressed_below_statistical_noise | boolean | Always 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_means | string | The honest phrasing for an empty /alerts response. |
| Field | Type | Meaning |
|---|---|---|
| generated_at | string | — |
| brand | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.idAlways present | string | Pass as ?brand=. |
| brand.nameAlways present | string | Display name. |
| note | string | — |
{
"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.
| Field | Type | Meaning |
|---|---|---|
| idAlways present | string | The workspace public id. Pass as ?brand=. |
| nameAlways present | string | Brand name. |
| domain | string | null | The brand's own domain, null before setup finishes. |
| configuredAlways present | boolean | False 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 present | integer | Prompts currently running for this brand. |
| market | object | — |
| market.location_code | integer | null | DataForSEO location code. |
| market.language_code | string | null | ISO language code. |
| last_collected_at | string | null | ISO timestamp of the most recent stored answer. |
| Field | Type | Meaning |
|---|---|---|
| generated_at | string | — |
| brand | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.idAlways present | string | Pass as ?brand=. |
| brand.nameAlways present | string | Display name. |
| note | string | — |
| pinned | boolean | True when this key is bound to one brand. |
{
"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.
| Name | Type | Notes |
|---|---|---|
| days | integer | 1 to 365, default 7 |
| brand | string | Which 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. |
| Field | Type | Meaning |
|---|---|---|
| engines[] | object[] | — |
| engines[].engineAlways present | string | Stable slug, e.g. chatgpt. Use this for ?engine= filters. |
| engines[].nameAlways present | string | Display name, e.g. ChatGPT. |
| engines[].collection_methodAlways present | string | How this surface is read: api, scraper or serp. |
| engines[].presentAlways present | integer | Answers that named this brand. |
| engines[].responsesAlways present | integer | Answers in the window. The denominator, always returned. |
| engines[].rateAlways present | number | null | present/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 present | integer | The floor below which rate is null. |
| engines[].citations | integer | Answers from this engine that cited the brand's own domain. |
| engines[].avg_rank_when_named | number | null | Mean position in the list WHEN NAMED. Null when never named. |
| brands[] | object[] | — |
| brands[].nameAlways present | string | Brand name. |
| brands[].roleAlways present | "self" | "competitor" | Yours, or a rival. |
| brands[].presentAlways present | integer | Answers that named this brand. |
| brands[].responsesAlways present | integer | Answers in the window. The denominator, always returned. |
| brands[].rateAlways present | number | null | present/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 present | integer | The floor below which rate is null. |
| brands[].citations | integer | Answers citing this brand's own domain. |
| brands[].avg_rank_when_named | number | null | Mean list position when named. |
| brands[].framing | object | How the brand was FRAMED where it appeared. Deliberately not called a sentiment score: it counts cue phrases, it does not grade tone. |
| brands[].framing.classified | integer | Mentions we could classify at all. The denominator here. |
| brands[].framing.recommended | integer | Mentions framed as a recommendation. |
| brands[].framing.negative | integer | Mentions framed negatively. |
| Field | Type | Meaning |
|---|---|---|
| generated_at | string | — |
| brand | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.idAlways present | string | Pass as ?brand=. |
| brand.nameAlways present | string | Display name. |
| note | string | — |
| window_days | integer | — |
{
"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.
| Name | Type | Notes |
|---|---|---|
| limit | integer | 1 to 200, default 50 |
| cursor | string | An answer id from a previous page. |
| engine | string | Filter to one engine slug. |
| brand | string | Which 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. |
| Field | Type | Meaning |
|---|---|---|
| idAlways present | string | Opaque public id, ans_<12>. Also the pagination cursor. Never an integer. |
| engineAlways present | string | Engine slug. |
| collection_method | string | api, scraper or serp. |
| promptAlways present | string | The prompt that was asked. Called `prompt`, not `question`. |
| answer | string | null | The 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 present | string | ISO timestamp. |
| location_code | integer | null | The market this was collected in. |
| mentions[]Always present | object[] | — |
| mentions[].brandAlways present | string | Which tracked brand this refers to. |
| mentions[].roleAlways present | "self" | "competitor" | — |
| mentions[].mentionedAlways present | boolean | False rows exist: we looked and it was absent. |
| mentions[].rank | integer | null | Position in the answer's list, 1-based. Null when unranked. |
| mentions[].count | integer | null | Times named in this one answer. |
| mentions[].framing | string | null | recommended | neutral | negative | null when genuinely ambivalent. |
| mentions[].framing_signals | string[] | null | The cue phrases behind `framing`, so a judgement can be checked. |
| mentions[].snippet | string | null | The verbatim sentence naming the brand. This is the evidence. |
| citations[]Always present | object[] | — |
| citations[].urlAlways present | string | The cited page. |
| citations[].domainAlways present | string | Its host. |
| citations[].rank | integer | null | Position in the engine's source list. That order IS the rank. |
| citations[].title | string | null | Page title where the engine supplied one. |
| Field | Type | Meaning |
|---|---|---|
| generated_at | string | — |
| brand | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.idAlways present | string | Pass as ?brand=. |
| brand.nameAlways present | string | Display name. |
| note | string | — |
| next_cursor | string | null | Pass as ?cursor=. Null on the last page. |
{
"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.
| Name | Type | Notes |
|---|---|---|
| days | integer | 1 to 365, default 30 |
| brand | string | Which 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. |
| Field | Type | Meaning |
|---|---|---|
| domainAlways present | string | Normalised host — www.example.com and example.com are one row. |
| answers_citing_itAlways present | integer | Answers in the window that cited this domain. |
| answers_where_you_were_namedAlways present | integer | Of those, how many also named your brand. |
| answers_citing_you_without_naming_youAlways present | integer | The actionable number. A domain read on nine answers that named a competitor every time is a specific page to go and be on. |
| Field | Type | Meaning |
|---|---|---|
| generated_at | string | — |
| brand | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.idAlways present | string | Pass as ?brand=. |
| brand.nameAlways present | string | Display name. |
| note | string | — |
| window_days | integer | — |
| limit | integer | — |
{
"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.
| Name | Type | Notes |
|---|---|---|
| days | integer | 1 to 365, default 30 |
| quotes | "1" | Pass 1 to include up to three supporting sentences per competitor. Omitted entirely when not requested. |
| brand | string | Which 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. |
| Field | Type | Meaning |
|---|---|---|
| competitorAlways present | string | The competitor's name as tracked in this workspace. |
| domainAlways present | string | null | Their domain, when one is recorded. |
| answers_naming_themAlways present | integer | Answers in the window that named them. |
| answers_without_youAlways present | integer | The 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 present | integer | null | Best (lowest) position they reached. Null when no answer ranked them. |
| typical_list_positionAlways present | number | null | Median position to 1dp. Null when no answer ranked them. |
| total_mentionsAlways present | integer | Times named across those answers, which can exceed answers_naming_them. |
| enginesAlways present | string[] | 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 present | string | The sentence the engine wrote, with its ** emphasis left intact. |
| quotes[].engineAlways present | string | Engine slug the sentence came from. |
| quotes[].questionAlways present | string | The tracked prompt whose answer contained it. |
| quotes[].listRankAlways present | integer | null | Where the engine placed them; 1 is first. Null when it wrote no list. |
| quotes[].answerIdAlways present | string | The answer this came from, as returned by /answers. |
| Field | Type | Meaning |
|---|---|---|
| generated_at | string | — |
| brand | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.idAlways present | string | Pass as ?brand=. |
| brand.nameAlways present | string | Display name. |
| note | string | — |
| window_days | integer | — |
| total_answers | integer | Answers in the window, all engines. |
| answers_naming_you | integer | — |
| answers_without_you | integer | total_answers - answers_naming_you. |
{
"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.
| Name | Type | Notes |
|---|---|---|
| days | integer | 1 to 365, default 30 |
| limit | integer | 1 to 50, default 12 |
| brand | string | Which 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. |
| Field | Type | Meaning |
|---|---|---|
| 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 present | integer | Sort 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 present | string | The registrable site. One publisher, one outreach contact. |
| pages[]Always present | object[] | The specific pages, because 'get listed on medium.com' is advice and naming the roundup is a task. Never empty. |
| pages[].urlAlways present | string | — |
| pages[].titleAlways present | string | null | — |
| answersAlways present | integer | Answers in the window that cited this site. |
| answers_naming_youAlways present | integer | Of those, how many named you. Zero for every gap, by definition. |
| competitors_namedAlways present | integer | Distinct tracked competitors named in those answers. |
| enginesAlways present | string[] | Which engines read it. Listed, never pooled into one figure. |
| shareAlways present | number | Fraction of meta.total_answers this door accounts for. Meaningless without that denominator, which is why it is returned beside it. |
| Field | Type | Meaning |
|---|---|---|
| generated_at | string | — |
| brand | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.idAlways present | string | Pass as ?brand=. |
| brand.nameAlways present | string | Display name. |
| note | string | — |
| window_days | integer | — |
| total_answers | integer | The denominator for every share above. |
| displayable | boolean | False when the corpus is too thin to rank anything honestly. An empty list with displayable false means NOT YET, never NOTHING TO DO. |
{
"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.
| Name | Type | Notes |
|---|---|---|
| brand | string | Which 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. |
| Field | Type | Meaning |
|---|---|---|
| idAlways present | string | Public id. |
| textAlways present | string | — |
| categoryAlways present | string | null | — |
| sourceAlways present | string | How the question got here, for example user or suggested. |
| activeAlways present | boolean | Inactive questions stop being run but keep the answers they already produced. |
| answersAlways present | integer | Answers this question has produced in total, all engines. |
| answers_naming_youAlways present | integer | — |
| web_search_volumeAlways present | integer | null | NULL 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 present | string | null | When volume was last looked up. Null when it never has been. |
| Field | Type | Meaning |
|---|---|---|
| generated_at | string | — |
| brand | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.idAlways present | string | Pass as ?brand=. |
| brand.nameAlways present | string | Display name. |
| note | string | — |
| questions_allowed | integer | The plan's cap on active questions, organisation-wide. |
| questions_used | integer | — |
| questions_remaining | integer | Zero means another question cannot be added on this plan. |
{
"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.
| Name | Type | Notes |
|---|---|---|
| days | integer | 1 to 365, default 30 |
| brand | string | Which 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. |
| Field | Type | Meaning |
|---|---|---|
| brandAlways present | string | The brand this row describes. |
| is_youAlways present | boolean | True for the workspace's own brand. |
| answers_naming_themAlways present | integer | Answers that named this brand. NOT the denominator. |
| classifiedAlways present | integer | THE 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 present | integer | Of the classified mentions, how many read as a recommendation. |
| negativeAlways present | integer | Of the classified mentions, how many read as negative. |
| neutralAlways present | integer | classified - recommended - negative, floored at 0. |
| recommended_shareAlways present | number | null | recommended/classified to 3dp, or NULL below min_sample (5) classified mentions. Null means NOT ENOUGH CLASSIFIED MENTIONS and must never be rendered as 0%. |
| Field | Type | Meaning |
|---|---|---|
| generated_at | string | — |
| brand | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.idAlways present | string | Pass as ?brand=. |
| brand.nameAlways present | string | Display name. |
| note | string | — |
| window_days | integer | — |
| min_sample | integer | Classified-mention floor below which recommended_share is null. |
{
"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/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.
| Name | Type | Notes |
|---|---|---|
| limit | integer | 1 to 200, default 50 |
| brand | string | Which 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. |
| Field | Type | Meaning |
|---|---|---|
| idAlways present | string | Opaque public id, alr_<12>. Use it to dedupe across polls. |
| kindAlways present | string | Finding type, e.g. visibility_drop. |
| titleAlways present | string | One line. |
| detail | string | The explanation, including the numbers behind it. |
| facts | object | The figures the finding was computed from. |
| created_atAlways present | string | ISO timestamp. |
| readAlways present | boolean | Whether it has been seen in the dashboard. |
| Field | Type | Meaning |
|---|---|---|
| generated_at | string | — |
| brand | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.idAlways present | string | Pass as ?brand=. |
| brand.nameAlways present | string | Display name. |
| note | string | — |
{
"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
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
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
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?
Does the AIMentionTracker API have write endpoints?
How does pagination work in the AIMentionTracker API?
Why is rate sometimes null in the API response?
Can I get one overall AI visibility score from the API?
Does the API count AIMentionTracker outages as my brand being absent?
Is the AIMentionTracker API documented with OpenAPI?
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.