How the data model nests
Organization, domains, topics, prompts, and where each analysis attaches — the mental model behind every recipe
Most "why is this field empty?" questions come down to reading a value at the wrong level of the hierarchy — this page maps what nests inside what, and which analysis hangs off which node.
The hierarchy
Everything your API key reaches nests in one chain:
- Organization owns everything the key can reach.
- Domain — one brand you track.
- Topic — holds prompts, market, language, and competitors.
- Prompt — a question asked of the models; each run produces a report.
Most recipes resolve a domainId, then a topicId, then query an analysis endpoint scoped to one of those. Identifiers are opaque strings — always read the id field, never parse or build it. Resolve them once with Find your domain id and List topics & competitors, then reuse them.
What lives on the domain
The domain object (GET /domains, GET /domains/{domainId}) carries brand-wide config:
ownedSources[]— domains you own (site plus properties). Together with the domainurlthey define whatdomainSourceAnalysisandfilterOwnership=ownedcount as yours. Entries are stored as bare hosts and matched per host, subdomains included, so a path such asexample.com/fr/is the same property asexample.com.personas[]— free-text descriptions of who the prompts speak for. They steer prompt generation only; they are not a segmentation and never become tags on their own (see below).aliases[]— alternative brand names for matching mentions, so "Nike" and "Nike Inc." count as one.marketLanguageCodePairs[]— the market/language combos analysed, each{ id, market, languageCode }. Filter aggregated endpoints withmarketLanguageCodePairId= the pair'sid, formatted<marketCode>-<languageCode>(e.g.US-en,FR-fr).
There are no competitors at the domain level — the most common wrong assumption.
What lives on the topic
Competitors are configured per topic (GET /domains/{domainId}/topics); each topic carries its own competitors[], keywords[], market, and language.
- Market and language sit on the topic, so a domain tracked in several markets has parallel topics — one set per market — not one topic with many locales.
- The competitor names are load-bearing: they key the
competitorsmap in visibility responses (chartData[].competitors), so a comparison chart shows only competitors configured here first. - Names and aliases are unique within a topic, compared without case, spacing or punctuation: an alias that repeats another competitor's name is refused, because a mention matching it would be credited to the alias owner.
- Editing a topic — competitors, aliases, keywords — never triggers a new analysis; the next scheduled scan picks the change up. Creating a topic from the app does launch a full domain refresh right away;
POST /domains/{domainId}/topicsthrough the API does not.
Prompts and tags
Prompts are the questions asked of the models; their runs produce the reports every analysis endpoint reads. Tags label prompts and are the only segmentation the API exposes — there is no persona query parameter. Tags are created by you, in the app (Brand Setup → Tag Management) or with POST /domains/{domainId}/tags, and then assigned to prompts; nothing derives them from the domain's personas[], so a domain with personas and no tags returns an empty list.
List them with GET /domains/{domainId}/tags (a bare array of { id, name, ... }, empty when none exist), then pass their id values as tagIds (comma-separated, OR semantics) to scope a visibility query. A tag library belongs to one domain: the same name under two domains is two independent tags with different ids. See Segment Visibility.
Where each analysis attaches
- visibility and competition — topic-scoped:
GET /domains/{domainId}/topics/{topicId}/visibility/aggregated,.../competition/aggregated. - sentiment, alignment, and Shopping — domain-scoped:
.../sentiment/aggregated,.../alignment/aggregated,.../shopping/analytics/*. - Citations and sources — domain-scoped:
.../explorer/citations,.../sources/visibility. - Gaps — two different lists.
GET /domains/{domainId}/content-gapsis computed from the visibility runs (prompts where the brand was absent) and needs no setup.GET /domains/{domainId}/gapsonly holds the gaps someone saved for follow-up from the app or withPOST .../gaps, so it is empty until you use it.
A topicId in the path means a per-topic view; stopping at domainId aggregates across the brand.
One report per model
Every scan asks each tracked model separately, and each answer set is stored as its own report — so a brand tracked on four models produces four sentiment reports and four alignment reports per scan, not one.
GET /domains/{domainId}/sentiment/reportsand.../alignment/reportsreturn one entry per model per scan; readmodels[]to see which model an entry covers.- The aggregated endpoints average each model over its own reports and return the result in
modelBreakdown[]({ model, score, variation }), next to the blendedoverallScore(sentiment) oraverageScore(alignment). reportCounton an aggregated response counts reports, not scan dates — divide by the number of models to get the number of scans it spans.