DocsAPI & MCP

Resource reference

Every v1 REST resource. Request contract, response fields, types, and scales for brands, overview, prompts, competitors, sources, urls, answers, shopping, fact-check, ads, and crawler ingest.

All read resources share one request contract. The ingest resource is separate, at the end. Read Conventions first.

Shared request contract (read resources)

  • Method / path: GET /api/v1/{resource}
  • Auth: Authorization: Bearer <key> (see Authentication).
  • Query params: brand=<brandId> is optional and defaults to your first brand. There are no date parameters. The window is fixed at 7 days. List resources also take optional cursor and limit params (additive, W5), see Limits. Leave them off and you get the pre-pagination response.
  • Response: JSON. Metric fields follow the scale conventions. Nullable fields come back as null, never 0. Every /api/v1 and /api/mcp response carries the X-MF-API-Version header, see the Changelog.

The valid resources are: brands, overview, prompts, competitors, sources, urls, answers, shopping, shopping-trend, fact-check, ads.

Three additive endpoints sit outside the fixed 7-day contract and take window parameters: GET /api/v1/metrics/history, GET /api/v1/crawlers, and GET /api/v1/ads.

GET /api/v1/brands

Lists the brands your key can see. Takes no brand param.

{ "brands": [ { "id": "…", "name": "…", "domain": "…", "archived": false } ] }

archived (additive) is true for a brand whose collection was stopped in the project's Danger Zone. Archived brands never leave this list, and every read resource keeps answering for them. The flag says the data stops at the archive date. It hides nothing.

GET /api/v1/overview

The visibility scorecard for one brand over the 7-day window.

FieldTypeMeaning
brand{id,name,domain}the tracked brand
window_daysint7
run_countintblended runs in the window
visibilitynumber 0–100rounded to 0.1
presence_ratenumber 0–1rounded to 0.001
share_of_voicenumber 0–100deprecated, frozen; percentage, rounded to 0.1
share_of_voice_fractionnumber 0–1additive; same number on the fraction rule, rounded to 0.001
citation_sharenumber 0–1 | nullunrounded
sentiment_indexnumber 0–100 | null50 = neutral
avg_positionnumber | nullmean 1-based rank
low_confidencebooltrue when run_count < 5
has_prior_windowboolwhether a prior 7-day window has runs

GET /api/v1/prompts

{ brand, tag_groups: [...], prompts: [...] }: all active prompts for the brand, plus the brand's named tag groups.

FieldTypeMeaning
idstringprompt id
textstringthe prompt
stagestringfunnel stage label, or
topicstring | nullfrozen legacy field, the first tag
topic_idstring | nulltopic folder id (additive)
topic_namestring | nulltopic folder name (additive)
tagsstring[]tags
est_volumenumber | nulllegacy point volume estimate
est_volume_band[low,high] | nulldemand band (AI asks/mo)
volume_confidencestring | nullconfidence tier
matched_gsc_querystring | nullGSC query behind a measured tier
gsc_impressions_28dnumber | nullthat query's 28-day site impressions
presence_ratenumber 0–1rounded to 0.001
visibilitynumber 0–100rounded to integer
avg_positionnumber | nullmean 1-based rank

topic is frozen and stays the first tag. Folders arrive additively, as topic_id and topic_name, see Conventions.

The envelope also carries tag_groups (additive): the brand's named tag groups over the free-text prompt tags, in creation order.

FieldTypeMeaning
idstringtag-group id
namestringgroup name
colorstring#rrggbb hex
tagsstring[]member tags, limited to tags in use across this listing's prompts

A stored member whose tag no listed prompt carries anymore is dropped. tag_groups: [] means the brand has defined no groups, not that its prompts have no tags. The block rides both the plain and the paginated envelope, and is served alone as the list_tag_groups MCP tool. Prompt tags stay free text, and a tag in no group is simply ungrouped.

GET /api/v1/competitors

{ brand, competitors: [...] }: one row per tracked entity, including your own brand, which is flagged is_brand.

FieldTypeMeaning
idstringentity id
namestringentity name
domainstring"" if the competitor has none
is_brandbooltrue for your brand's row
visibilitynumber 0–100rounded to 0.1
share_of_voicenumber 0–100deprecated, frozen; percentage, rounded to 0.1
share_of_voice_fractionnumber 0–1additive; same number on the fraction rule, rounded to 0.001
citation_sharenumber 0–1 | nullunrounded
sentiment_indexnumber 0–100 | null
avg_positionnumber | nullmean 1-based rank

Every metric here, share of voice included, blends the eight search-grounded engines (see Conventions).

GET /api/v1/sources

{ brand, sources: [...] }: the top 20 cited domains, by citations descending.

FieldTypeMeaning
domainstringcited domain
classificationstringowned / competitor / editorial / ugc / …; the effective label, workspace reclassifications merged in
overriddenbooltrue when a workspace reclassification set it, not the automatic label
citationsinttotal citations to this domain in the window
brand_presentboolbrand mentioned in at least 1 run citing this domain

GET /api/v1/urls

{ brand, urls: [...] }: the top 50 cited URLs across all domains, by peak-window citations. Content keys merge YouTube watch?v= spellings and Reddit permalink variants. Elsewhere we strip only fragments and tracking parameters (utm_*/ref/fbclid/gclid), so query-keyed content like item?id= threads stays distinct.

FieldTypeMeaning
urlstringcanonical cited URL
domainstringits domain
titlestring | nullpage title when an engine reported one
classificationstringthe domain's effective classification, reclassifications merged
overriddenbooltrue when the classification is a workspace reclassification
citationsintcitations in the current 7-day window (0 = cited in the prior window only)
prior_citationsint | nullcitations in the prior 7 days; null = no prior window observed yet, not zero
answersintdistinct answers citing it in the current window
enginesstring[]engines citing it
brand_presentboolbrand mentioned in at least 1 answer citing this URL
page_typestring | nullpage type read from the URL path and stored title: listicle / comparison / howto / review / docs / forum / news / product; null = no recognizable signal, never a guessed default

GET /api/v1/answers

{ brand, answers: [...] }: up to 200 runs, newest first. Covers all engines, not just the blended ones.

FieldTypeMeaning
idstringrun id
datestring YYYY-MM-DDcollection date
regionstringe.g. us
enginestringone of the ten engine ids
modelstringmodel version ("" if unknown)
prompt_idstringsource prompt
brand_presentboolbrand mentioned in this run
mentions[{name,order,sentiment,evidence}]order 1-based; sentiment positive/neutral/negative/null; evidence (additive, 2026-08-23) the verbatim span the tone was read from, null when unscored
citations[{domain,url}]cited sources

GET /api/v1/metrics/history

Daily time series for one metric, the same series the dashboard charts (additive, W5). Unlike the resources above, it takes window parameters.

Query params:

ParamValuesMeaning
brandbrand idoptional; defaults to your first brand
metricvisibility | share_of_voice_fraction | citation_share | sentiment_indexrequired
days7 | 14 | 28 | 56preset window (default 28), anchored at the newest collected day. 56 is API-only (the dashboard range control tops out at 28) and spans the whole live run horizon, so its trend is always null
from, toYYYY-MM-DDcustom inclusive range instead of days; max 366 days
engineengine id, or a comma list (chatgpt,perplexity)engine filter (additive 2026-07-16). One id gives the released single-engine series. A comma list scopes to exactly those engines, model-knowledge engines included when listed. The default blends the search-grounded engines (see Conventions). Unknown ids are a 400
competitorstrueinclude tracked-competitor series alongside the brand

Response envelope:

FieldTypeMeaning
brand{id,name,domain}
metric / unitstringunit is score_0_100, fraction_0_1, or index_0_100
window{start,end,days}resolved bounds; start/end null when nothing was ever collected in scope
enginestring | nullfrozen echo: the engine when exactly one is filtered, else null. Multi-engine requests read engines
enginesstring[] | nulladditive (2026-07-16): the full filter set, null when unfiltered
engine_scopestringblended_search_grounded, single_engine, or engine_set (additive, 2 or more engines)
sourcestringruns = live-computed over the last 56 days, the dashboard's exact code path; daily_stats = nightly aggregate for older custom ranges
metrics_versionintcurrent metric-formula version
seriesarrayone entry per entity (below)
notestring | nullhonesty caveats: single-day window, empty scope, trend suppressed at the run horizon, preset axis clamped to the horizon, daily_stats limits

Each series[] entry:

FieldTypeMeaning
id / name / is_brandentity identity
points[{date,value,sample_size}]one point per calendar day. value null with sample_size 0 = nothing collected that day (null is not zero). value null with sample_size above 0 = collected but unmeasurable, e.g. no run cited any source
window_valuenumber | nullthe metric over the whole window, same math as the dashboard KPI; null on a daily_stats source
window_run_countintruns behind window_value
low_confidenceboolwindow_run_count < 5
trendobject | null{previous_window_value, delta, delta_unit} against the prior window of the same length. Null for single-day windows, on a daily_stats source, and whenever the prior window would fall outside the 56-day live run horizon (days=56 presets, custom ranges starting less than days after the horizon). A truncated baseline is never compared, and note says so

Scales: visibility and sentiment_index are 0 to 100 rounded to 0.1. Fraction metrics are 0 to 1 rounded to 0.001. delta_unit is relative_pct for visibility, fraction_points or index_points for the others.

On source: daily_stats, blended and multi-engine share_of_voice_fraction, citation_share and sentiment_index cannot be derived from the stored per-engine aggregates, because their denominators are not stored. The endpoint returns 400 and suggests a single engine= instead of inventing a pool. Pooled visibility IS served, blended or as a set: the run-count-weighted mean is identical to pooling the runs.

Also available as the get_metrics_history MCP tool.

GET /api/v1/crawlers

Agent analytics over the API (additive, W5): which AI agents read the brand's own site, from its connected crawler logs, plus each agent's robots.txt verdict. Same data as the /crawlers dashboard and its per-agent drill-down. Plan-gated like that page, Growth and above: a Starter or Trial key gets 403. The operator key is exempt.

Query params: brand, as everywhere, and days: 7 (default), 14, or 28.

Top level:

FieldTypeMeaning
brand{id,name,domain}
window_daysintecho of days
connectedboolfalse = no crawler logs have EVER been received for this brand
last_event_atstring | nullnewest event on record, unwindowed; null = never connected
robots_fetch_stateok | absent | errorabsent = no robots.txt, a real finding that nothing is blocked; error = couldn't check, so verdicts are null, not a green light
totals{hits, pages}window totals; null when connected is false: unknown, never a fabricated zero
agentsarrayevery known AI agent, plus anything else the logs carried (below), by hits descending
notestring | nullhonesty caveats: never connected; quiet pipe (nothing from ANY agent in 7 days, so zero hits may be missing data); robots fetch failure

Each agents[] entry:

FieldTypeMeaning
agentstringstable key (e.g. gptbot)
label / operatorstringdisplay name and the company running it
purposetraining | search_index | user_fetchblocking search_index or user_fetch agents removes you from answers, not just from training
enginestring | nullthe product engine this crawler feeds, when it maps cleanly
hits / pagesint | nullwindow activity; null when no logs were ever connected: unknown, not zero
first_seen / last_seenstring | nullwindow-scoped timestamps; null when this agent has no hits
robots{verdict, rule, blocked_path} | nullthis agent's robots.txt verdict (allowed/blocked/unaddressed); null when the fetch failed, meaning "couldn't check", never reassurance

Also available as the get_crawler_analytics MCP tool.

GET /api/v1/shopping

The shopping leaderboard over the 7-day window, from the same computation as the dashboard's shopping analytics. Metrics blend every collecting engine. The per-engine split is the channel_coverage block.

Top level:

FieldTypeMeaning
brand{id,name,domain}
window_daysint7
totals.carouselsintdistinct carousels captured
totals.productsintdistinct products
totals.own_appearancesintown-product placements
totals.own_avg_positionnumber | nullnull = own never appeared
productsarrayup to 30 (below)
channel_coveragearrayper-engine shopping capability (additive; below)
notestring | nullabsence explainer (see Conventions)

Each channel_coverage[] entry describes one shopping-capable engine:

FieldTypeMeaning
enginestringengine id (chatgpt, aio, aimode, copilot, perplexity, gemini)
statusstringparses-today: the engine's payloads carry parseable product data. awaiting-payload: extraction is wired, but the data vendor sends no product fields yet
sampledboolthe engine collected runs in this window
carouselsint | nullcarousels observed this window. null exactly when unobservable: the engine wasn't sampled, or the payload is awaiting the vendor. Null is not zero. A 0 means a sampled, parsing engine showed no carousel

Engines with no shopping surface (Claude, DeepSeek, Grok, Mistral) are not listed. See Engine coverage for the product-side view of the same map.

Each products[] entry:

FieldTypeMeaning
productstringproduct name
merchant_domainstring | nullderived from the product link only
merchantstring | nullverbatim engine merchant text (usually null)
pricestring | nullthe engine's verbatim price text; null when the engine sent none
price_amountnumber | nullexact amount, from the payload's structured price fields when the engine provides them, else parsed once from price; null = unknown, never guessed
currencystring | nullcurrency of price_amount; null when the payload didn't pin one
urlstring | nullproduct link
is_ownboolyour own product
appearancesinttotal placements
carousels_featuringintdistinct carousels featuring it
win_ratenumber 0–1share of its appearances in slot #1
visibility_sharenumber 0–1carousels featuring it divided by window carousels
avg_positionnumbermean slot (1 = front of shelf)

The price fields are independent. Some engines send only structured numbers, so price can be null while price_amount and currency are set. To show a price, format it from price_amount and currency, the way the dashboard does. All three null means the engine sent nothing.

GET /api/v1/shopping-trend

{ brand, window_days, days_observed, trend: [...], note }. days_observed counts collected days. Days without carousels produce no point.

Each trend[] point:

FieldTypeMeaning
datestring YYYY-MM-DD
carouselsintcarousels captured that day
own_carousel_sharenumber 0–1that day's carousels featuring an own product
own_win_sharenumber 0–1that day's carousels with an own product in slot #1
own_avg_positionnumber | nullnull on days you weren't shelved

note flags a single-day snapshot. Check days_observed >= 2 before you chart a line.

GET /api/v1/fact-check

The brand's latest knowledge-base fact check (additive, 2026-07-18): where AI engines' claims about the brand contradict its own knowledge base. This is not a windowed view. The check runs on demand, from the Answers page or the run_fact_check MCP tool. This resource returns the single cached result, which each run overwrites. We check engine answers against your knowledge base, not against general truth.

{ brand, fact_check, note }: fact_check is null when no check has ever run. Read that as "never checked", not "no discrepancies", and note says so.

fact_check fields:

FieldTypeMeaning
checked_atstring ISOwhen the check ran
answers_checkedintengine answers compared: up to 8, the newest per engine, and only ones that mention the brand
stalebooltrue once the result is 7+ days old. A full collection week has passed, so every engine has answered again. Results never refresh on their own
age_daysint | nullwhole days since the check; null only on an unparsable stored stamp
stale_after_daysint7, the staleness rule, stated so you don't hard-code it
cost_usdnumber | nullthe metered LLM spend of that run; null on results stored before metering landed: unknown, never a fabricated 0
discrepanciesarray[{engine, claim, truth, severity}]. claim is what the engine said, truth is what the knowledge base says, severity is high/medium/low. Empty = the checked answers matched the knowledge base

There is no REST trigger. A check spends a metered LLM call, so the write stays on the dashboard card and the run_fact_check MCP tool, which has a fresh-cache cost guard. Also available as the get_fact_check MCP tool.

GET /api/v1/ads

Captured advertiser units (additive, 2026-07-21): the engine-stated sponsored placements rendered with AI answers on the brand's tracked prompts. This is the durable ad_intel capture behind the Ads intelligence page. Identity comes from the engine's own payload at collection time, never inferred from answer text. Unlike the fixed-window resources it takes window parameters, on the metrics-history grammar.

Query params:

ParamValuesMeaning
brandbrand idoptional; defaults to your first brand
days7 | 14 | 28 | 56preset window (default 7), anchored at the brand's newest captured unit
from, toYYYY-MM-DDcustom inclusive range instead of days; max 366 days
engineengine id, or a comma listoptional filter; unknown ids are a 400

Top level:

FieldTypeMeaning
brand{id,name,domain}
window{start,end,days}resolved bounds; start/end null when nothing was ever captured, so a preset has no anchor
enginesstring[] | nullthe engine filter echo; null = unfiltered
engines_observablestring[]engines whose payloads state ad presence at all: ChatGPT only today. Answers from any other engine are an unknown here, never "ad-free"
totals{units, advertisers}window totals with the engine filter applied, counted before the row cap. units equals answers-with-ads: engines state at most one advertiser unit per answer
capture{all_time_units, first_captured_at, last_captured_at}the brand's real all-time capture bounds. Structured capture began July 2026, so an empty earlier window means "not captured", never "no ads ran"
adsarraynewest first, capped at 200 (note says when the cap bit)
notestring | nullhonesty caveats: never captured, none in window, non-reporting engine filter, row cap

Each ads[] entry is one answer's unit, verbatim from capture:

FieldTypeMeaning
run_id / prompt_idstringthe answer and prompt the unit rendered with (run_id joins /api/v1/answers rows)
enginestringthe engine that stated the unit
captured_atstring ISOthe answer's collection time, copied from the run, so backfilled history windows stay honest
datestring YYYY-MM-DDits day
advertiserstring | nullengine-stated advertiser name; null = the unit stated no name: "not stated", never guessed
advertiser_url / advertiser_domainstring | nullthe stated link and its hostname; null = no link
favicon_urlstring | nullvendor-served favicon, when stated
cards[{title, body, target_url, domain, image_url}]the unit's creative cards, verbatim; [] = advertiser stated with no cards

There are no spend estimates by design: we have no grounded signal to model ad spend from. Also available as the list_ads MCP tool.

GET /api/v1/sentiment

How the engines talk about you and every tracked rival, with receipts (additive, 2026-08-23). One window, the brand's engine scope, and the same 0 to 100 index the dashboards show. Also available as the get_sentiment MCP tool.

Query params:

ParamValuesMeaning
brandbrand idoptional; defaults to your first brand
days7 | 14 | 28 | 56preset window (default 7), anchored at the brand's newest collected day
engineengine id, or a comma listoptional. Omit for the blended search-grounded scope every headline metric uses. Pass ids to scope to exactly those engines, knowledge engines included

Top level:

FieldTypeMeaning
brand{id,name,domain}
window_daysnumberthe preset used
engines / engine_scopestring[] | null, blended | engine_setthe filter echo and which scope rule applied
run_countnumberruns in the window under that scope. 0 means nothing was measured
entitiesarrayyou first, then every tracked competitor (see below)
by_enginearrayyour split per engine that ran: engine plus the entity fields
quotesarraythe receipts, worst first, capped at 40; quotes_total is the uncapped count
notestringthe reading rules, in words

Each entities[] entry:

FieldTypeMeaning
name, domain, is_brandwho
sentiment_indexnumber | null0 to 100, 50 = neutral: positive minus negative over scored mentions. null = no scored mention, never neutral
mentionsnumberanswers that named this entity
positive, neutral, negativenumberthe scored split the index came from
unscorednumbermentions stored with no tone (a degraded run). Reported, never folded into neutral, excluded from the index

Each quotes[] entry: run_id, date, engine, prompt_id, prompt, entity (whose tone it is; rivals and untracked names included), is_brand, tracked (false = a name the answers use that you do not track), sentiment, and quote, the verbatim span. Sorted negative, then neutral, then positive, newest first inside a tone.

POST /api/v1/crawler-events

The one write endpoint. It takes AI-crawler logs. Reachable by full and ingest keys (see Crawler ingest).

  • Path: POST /api/v1/crawler-events?brand=<id> (brand optional; defaults to first).
  • Body: raw text, either nginx/apache combined log lines or NDJSON (Cloudflare Logpush or Vercel Log Drains). Up to 10 MB per request. Only recognised AI agents are stored, paths only.
  • Success 200:
{ "inserted": 0, "matched": 0, "totalLines": 0, "unparsed": 0 }
FieldMeaning
insertedrows actually written (duplicates skipped)
matchedrecognised AI-crawler hits
totalLineslines that parsed as requests
unparsedlines that couldn't be parsed