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 optionalcursorandlimitparams (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, never0. Every/api/v1and/api/mcpresponse carries theX-MF-API-Versionheader, 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.
| Field | Type | Meaning |
|---|---|---|
brand | {id,name,domain} | the tracked brand |
window_days | int | 7 |
run_count | int | blended runs in the window |
visibility | number 0–100 | rounded to 0.1 |
presence_rate | number 0–1 | rounded to 0.001 |
share_of_voice | number 0–100 | deprecated, frozen; percentage, rounded to 0.1 |
share_of_voice_fraction | number 0–1 | additive; same number on the fraction rule, rounded to 0.001 |
citation_share | number 0–1 | null | unrounded |
sentiment_index | number 0–100 | null | 50 = neutral |
avg_position | number | null | mean 1-based rank |
low_confidence | bool | true when run_count < 5 |
has_prior_window | bool | whether 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.
| Field | Type | Meaning |
|---|---|---|
id | string | prompt id |
text | string | the prompt |
stage | string | funnel stage label, or — |
topic | string | null | frozen legacy field, the first tag |
topic_id | string | null | topic folder id (additive) |
topic_name | string | null | topic folder name (additive) |
tags | string[] | tags |
est_volume | number | null | legacy point volume estimate |
est_volume_band | [low,high] | null | demand band (AI asks/mo) |
volume_confidence | string | null | confidence tier |
matched_gsc_query | string | null | GSC query behind a measured tier |
gsc_impressions_28d | number | null | that query's 28-day site impressions |
presence_rate | number 0–1 | rounded to 0.001 |
visibility | number 0–100 | rounded to integer |
avg_position | number | null | mean 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.
| Field | Type | Meaning |
|---|---|---|
id | string | tag-group id |
name | string | group name |
color | string | #rrggbb hex |
tags | string[] | 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.
| Field | Type | Meaning |
|---|---|---|
id | string | entity id |
name | string | entity name |
domain | string | "" if the competitor has none |
is_brand | bool | true for your brand's row |
visibility | number 0–100 | rounded to 0.1 |
share_of_voice | number 0–100 | deprecated, frozen; percentage, rounded to 0.1 |
share_of_voice_fraction | number 0–1 | additive; same number on the fraction rule, rounded to 0.001 |
citation_share | number 0–1 | null | unrounded |
sentiment_index | number 0–100 | null | — |
avg_position | number | null | mean 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.
| Field | Type | Meaning |
|---|---|---|
domain | string | cited domain |
classification | string | owned / competitor / editorial / ugc / …; the effective label, workspace reclassifications merged in |
overridden | bool | true when a workspace reclassification set it, not the automatic label |
citations | int | total citations to this domain in the window |
brand_present | bool | brand 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.
| Field | Type | Meaning |
|---|---|---|
url | string | canonical cited URL |
domain | string | its domain |
title | string | null | page title when an engine reported one |
classification | string | the domain's effective classification, reclassifications merged |
overridden | bool | true when the classification is a workspace reclassification |
citations | int | citations in the current 7-day window (0 = cited in the prior window only) |
prior_citations | int | null | citations in the prior 7 days; null = no prior window observed yet, not zero |
answers | int | distinct answers citing it in the current window |
engines | string[] | engines citing it |
brand_present | bool | brand mentioned in at least 1 answer citing this URL |
page_type | string | null | page 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.
| Field | Type | Meaning |
|---|---|---|
id | string | run id |
date | string YYYY-MM-DD | collection date |
region | string | e.g. us |
engine | string | one of the ten engine ids |
model | string | model version ("" if unknown) |
prompt_id | string | source prompt |
brand_present | bool | brand 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:
| Param | Values | Meaning |
|---|---|---|
brand | brand id | optional; defaults to your first brand |
metric | visibility | share_of_voice_fraction | citation_share | sentiment_index | required |
days | 7 | 14 | 28 | 56 | preset 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, to | YYYY-MM-DD | custom inclusive range instead of days; max 366 days |
engine | engine 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 |
competitors | true | include tracked-competitor series alongside the brand |
Response envelope:
| Field | Type | Meaning |
|---|---|---|
brand | {id,name,domain} | — |
metric / unit | string | unit 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 |
engine | string | null | frozen echo: the engine when exactly one is filtered, else null. Multi-engine requests read engines |
engines | string[] | null | additive (2026-07-16): the full filter set, null when unfiltered |
engine_scope | string | blended_search_grounded, single_engine, or engine_set (additive, 2 or more engines) |
source | string | runs = live-computed over the last 56 days, the dashboard's exact code path; daily_stats = nightly aggregate for older custom ranges |
metrics_version | int | current metric-formula version |
series | array | one entry per entity (below) |
note | string | null | honesty caveats: single-day window, empty scope, trend suppressed at the run horizon, preset axis clamped to the horizon, daily_stats limits |
Each series[] entry:
| Field | Type | Meaning |
|---|---|---|
id / name / is_brand | — | entity 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_value | number | null | the metric over the whole window, same math as the dashboard KPI; null on a daily_stats source |
window_run_count | int | runs behind window_value |
low_confidence | bool | window_run_count < 5 |
trend | object | 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:
| Field | Type | Meaning |
|---|---|---|
brand | {id,name,domain} | — |
window_days | int | echo of days |
connected | bool | false = no crawler logs have EVER been received for this brand |
last_event_at | string | null | newest event on record, unwindowed; null = never connected |
robots_fetch_state | ok | absent | error | absent = 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 |
agents | array | every known AI agent, plus anything else the logs carried (below), by hits descending |
note | string | null | honesty 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:
| Field | Type | Meaning |
|---|---|---|
agent | string | stable key (e.g. gptbot) |
label / operator | string | display name and the company running it |
purpose | training | search_index | user_fetch | blocking search_index or user_fetch agents removes you from answers, not just from training |
engine | string | null | the product engine this crawler feeds, when it maps cleanly |
hits / pages | int | null | window activity; null when no logs were ever connected: unknown, not zero |
first_seen / last_seen | string | null | window-scoped timestamps; null when this agent has no hits |
robots | {verdict, rule, blocked_path} | null | this 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:
| Field | Type | Meaning |
|---|---|---|
brand | {id,name,domain} | — |
window_days | int | 7 |
totals.carousels | int | distinct carousels captured |
totals.products | int | distinct products |
totals.own_appearances | int | own-product placements |
totals.own_avg_position | number | null | null = own never appeared |
products | array | up to 30 (below) |
channel_coverage | array | per-engine shopping capability (additive; below) |
note | string | null | absence explainer (see Conventions) |
Each channel_coverage[] entry describes one shopping-capable engine:
| Field | Type | Meaning |
|---|---|---|
engine | string | engine id (chatgpt, aio, aimode, copilot, perplexity, gemini) |
status | string | parses-today: the engine's payloads carry parseable product data. awaiting-payload: extraction is wired, but the data vendor sends no product fields yet |
sampled | bool | the engine collected runs in this window |
carousels | int | null | carousels 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:
| Field | Type | Meaning |
|---|---|---|
product | string | product name |
merchant_domain | string | null | derived from the product link only |
merchant | string | null | verbatim engine merchant text (usually null) |
price | string | null | the engine's verbatim price text; null when the engine sent none |
price_amount | number | null | exact amount, from the payload's structured price fields when the engine provides them, else parsed once from price; null = unknown, never guessed |
currency | string | null | currency of price_amount; null when the payload didn't pin one |
url | string | null | product link |
is_own | bool | your own product |
appearances | int | total placements |
carousels_featuring | int | distinct carousels featuring it |
win_rate | number 0–1 | share of its appearances in slot #1 |
visibility_share | number 0–1 | carousels featuring it divided by window carousels |
avg_position | number | mean 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:
| Field | Type | Meaning |
|---|---|---|
date | string YYYY-MM-DD | — |
carousels | int | carousels captured that day |
own_carousel_share | number 0–1 | that day's carousels featuring an own product |
own_win_share | number 0–1 | that day's carousels with an own product in slot #1 |
own_avg_position | number | null | null 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:
| Field | Type | Meaning |
|---|---|---|
checked_at | string ISO | when the check ran |
answers_checked | int | engine answers compared: up to 8, the newest per engine, and only ones that mention the brand |
stale | bool | true 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_days | int | null | whole days since the check; null only on an unparsable stored stamp |
stale_after_days | int | 7, the staleness rule, stated so you don't hard-code it |
cost_usd | number | null | the metered LLM spend of that run; null on results stored before metering landed: unknown, never a fabricated 0 |
discrepancies | array | [{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:
| Param | Values | Meaning |
|---|---|---|
brand | brand id | optional; defaults to your first brand |
days | 7 | 14 | 28 | 56 | preset window (default 7), anchored at the brand's newest captured unit |
from, to | YYYY-MM-DD | custom inclusive range instead of days; max 366 days |
engine | engine id, or a comma list | optional filter; unknown ids are a 400 |
Top level:
| Field | Type | Meaning |
|---|---|---|
brand | {id,name,domain} | — |
window | {start,end,days} | resolved bounds; start/end null when nothing was ever captured, so a preset has no anchor |
engines | string[] | null | the engine filter echo; null = unfiltered |
engines_observable | string[] | 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" |
ads | array | newest first, capped at 200 (note says when the cap bit) |
note | string | null | honesty caveats: never captured, none in window, non-reporting engine filter, row cap |
Each ads[] entry is one answer's unit, verbatim from capture:
| Field | Type | Meaning |
|---|---|---|
run_id / prompt_id | string | the answer and prompt the unit rendered with (run_id joins /api/v1/answers rows) |
engine | string | the engine that stated the unit |
captured_at | string ISO | the answer's collection time, copied from the run, so backfilled history windows stay honest |
date | string YYYY-MM-DD | its day |
advertiser | string | null | engine-stated advertiser name; null = the unit stated no name: "not stated", never guessed |
advertiser_url / advertiser_domain | string | null | the stated link and its hostname; null = no link |
favicon_url | string | null | vendor-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:
| Param | Values | Meaning |
|---|---|---|
brand | brand id | optional; defaults to your first brand |
days | 7 | 14 | 28 | 56 | preset window (default 7), anchored at the brand's newest collected day |
engine | engine id, or a comma list | optional. Omit for the blended search-grounded scope every headline metric uses. Pass ids to scope to exactly those engines, knowledge engines included |
Top level:
| Field | Type | Meaning |
|---|---|---|
brand | {id,name,domain} | — |
window_days | number | the preset used |
engines / engine_scope | string[] | null, blended | engine_set | the filter echo and which scope rule applied |
run_count | number | runs in the window under that scope. 0 means nothing was measured |
entities | array | you first, then every tracked competitor (see below) |
by_engine | array | your split per engine that ran: engine plus the entity fields |
quotes | array | the receipts, worst first, capped at 40; quotes_total is the uncapped count |
note | string | the reading rules, in words |
Each entities[] entry:
| Field | Type | Meaning |
|---|---|---|
name, domain, is_brand | who | |
sentiment_index | number | null | 0 to 100, 50 = neutral: positive minus negative over scored mentions. null = no scored mention, never neutral |
mentions | number | answers that named this entity |
positive, neutral, negative | number | the scored split the index came from |
unscored | number | mentions 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>(brandoptional; 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 }
| Field | Meaning |
|---|---|
inserted | rows actually written (duplicates skipped) |
matched | recognised AI-crawler hits |
totalLines | lines that parsed as requests |
unparsed | lines that couldn't be parsed |
Related
- MCP server: the same reads as agent tools.
- Conventions, Limits, Errors.