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.

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.brandtells 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 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 /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
organization●object—
organization.namestring | nullOrganisation name.
plan●object—
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.
key●object—
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.
brand●objectWhich brand a response describes. Present in meta on every data endpoint.
brand.id●stringPass as ?brand=.
brand.name●stringDisplay name.
rate_limit●object—
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_rules●objectNOT 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.id●stringPass as ?brand=.
brand.name●stringDisplay 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
id●stringThe workspace public id. Pass as ?brand=.
name●stringBrand name.
domainstring | nullThe brand's own domain, null before setup finishes.
configured●booleanFalse 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●integerPrompts 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.id●stringPass as ?brand=.
brand.name●stringDisplay 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[].engine●stringStable slug, e.g. chatgpt. Use this for ?engine= filters.
engines[].name●stringDisplay name, e.g. ChatGPT.
engines[].collection_method●stringHow this surface is read: api, scraper or serp.
engines[].present●integerAnswers that named this brand.
engines[].responses●integerAnswers in the window. The denominator, always returned.
engines[].rate●number | 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_sample●integerThe 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[].name●stringBrand name.
brands[].role●"self" | "competitor"Yours, or a rival.
brands[].present●integerAnswers that named this brand.
brands[].responses●integerAnswers in the window. The denominator, always returned.
brands[].rate●number | 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_sample●integerThe 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.id●stringPass as ?brand=.
brand.name●stringDisplay 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
id●stringOpaque public id, ans_<12>. Also the pagination cursor. Never an integer.
engine●stringEngine slug.
collection_methodstringapi, scraper or serp.
prompt●stringThe prompt that was asked. Called `prompt`, not `question`.
answerstring | nullThe 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●stringISO timestamp.
location_codeinteger | nullThe market this was collected in.
mentions[]●object[]—
mentions[].brand●stringWhich tracked brand this refers to.
mentions[].role●"self" | "competitor"—
mentions[].mentioned●booleanFalse 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[]●object[]—
citations[].url●stringThe cited page.
citations[].domain●stringIts 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.id●stringPass as ?brand=.
brand.name●stringDisplay 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
domain●stringNormalised host — www.example.com and example.com are one row.
answers_citing_it●integerAnswers in the window that cited this domain.
answers_where_you_were_named●integerOf those, how many also named your brand.
answers_citing_you_without_naming_you●integerThe 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.id●stringPass as ?brand=.
brand.name●stringDisplay 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 /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
id●stringOpaque public id, alr_<12>. Use it to dedupe across polls.
kind●stringFinding type, e.g. visibility_drop.
title●stringOne line.
detailstringThe explanation, including the numbers behind it.
factsobjectThe figures the finding was computed from.
created_at●stringISO timestamp.
read●booleanWhether 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.id●stringPass as ?brand=.
brand.name●stringDisplay 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.

Questions about the AI mention tracking API

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 six 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

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.