Docs
AIMentionTracker API reference
Six read-only endpoints over what we have already collected. One header, one envelope, and a worked response for every one of them.
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.
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.brandtells 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 six 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 /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 |
|---|---|---|
| organization● | object | — |
| organization.name | string | null | Organisation name. |
| plan● | 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. |
| key● | 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. |
| brand● | object | Which brand a response describes. Present in meta on every data endpoint. |
| brand.id● | string | Pass as ?brand=. |
| brand.name● | string | Display name. |
| rate_limit● | 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_rules● | 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.id● | string | Pass as ?brand=. |
| brand.name● | 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 |
|---|---|---|
| id● | string | The workspace public id. Pass as ?brand=. |
| name● | string | Brand name. |
| domain | string | null | The brand's own domain, null before setup finishes. |
| configured● | 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_prompts● | 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.id● | string | Pass as ?brand=. |
| brand.name● | 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[].engine● | string | Stable slug, e.g. chatgpt. Use this for ?engine= filters. |
| engines[].name● | string | Display name, e.g. ChatGPT. |
| engines[].collection_method● | string | How this surface is read: api, scraper or serp. |
| engines[].present● | integer | Answers that named this brand. |
| engines[].responses● | integer | Answers in the window. The denominator, always returned. |
| engines[].rate● | 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_sample● | 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[].name● | string | Brand name. |
| brands[].role● | "self" | "competitor" | Yours, or a rival. |
| brands[].present● | integer | Answers that named this brand. |
| brands[].responses● | integer | Answers in the window. The denominator, always returned. |
| brands[].rate● | 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_sample● | 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.id● | string | Pass as ?brand=. |
| brand.name● | 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 |
|---|---|---|
| id● | string | Opaque public id, ans_<12>. Also the pagination cursor. Never an integer. |
| engine● | string | Engine slug. |
| collection_method | string | api, scraper or serp. |
| prompt● | string | The prompt that was asked. Called `prompt`, not `question`. |
| answer | string | null | The answer verbatim, markdown. Null when the engine returned none. |
| outcome● | "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_at● | string | ISO timestamp. |
| location_code | integer | null | The market this was collected in. |
| mentions[]● | object[] | — |
| mentions[].brand● | string | Which tracked brand this refers to. |
| mentions[].role● | "self" | "competitor" | — |
| mentions[].mentioned● | 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[]● | object[] | — |
| citations[].url● | string | The cited page. |
| citations[].domain● | 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.id● | string | Pass as ?brand=. |
| brand.name● | 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 |
|---|---|---|
| domain● | string | Normalised host — www.example.com and example.com are one row. |
| answers_citing_it● | integer | Answers in the window that cited this domain. |
| answers_where_you_were_named● | integer | Of those, how many also named your brand. |
| answers_citing_you_without_naming_you● | 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.id● | string | Pass as ?brand=. |
| brand.name● | 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 /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 |
|---|---|---|
| id● | string | Opaque public id, alr_<12>. Use it to dedupe across polls. |
| kind● | string | Finding type, e.g. visibility_drop. |
| title● | string | One line. |
| detail | string | The explanation, including the numbers behind it. |
| facts | object | The figures the finding was computed from. |
| created_at● | string | ISO timestamp. |
| read● | 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.id● | string | Pass as ?brand=. |
| brand.name● | 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.
Questions about the AI mention tracking API
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
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.