API Integration
AI Visibility Analytics
What AI models said about you — mention rate, share of voice, and the three citation metric families, with the raw counts behind every number.
Everything under /org/analytics is read-only and scoped to the organization the key
belongs to. It needs Senso's GEO product on your organization; without it, a request answers 403.
curl "https://apiv2.senso.ai/api/v1/org/analytics/summary" \
-H "X-API-Key: $SENSO_API_KEY"
The shape of a response
Most endpoints return these blocks:
| Block | What it holds |
|---|---|
window | The effective date range: from, to, days, and latest_data_day, the most recent day with data for your model and location filter |
totals | Every raw count — all numerators and all denominators |
metrics | Every ratio derived from totals, each as { value, display } |
data_quality | A low / medium / high confidence flag from the answered-run count |
notes + definitions | Prose caveats and one-line metric definitions, generated per response |
/summary adds previous_window and deltas, the time series add series, and the lists add their rows with total, limit and offset. The citation-source endpoints carry their two denominators in a denominators block instead of totals and metrics. /answers/latest returns its answers with notes and definitions only.A ratio is null whenever its denominator is zero. Null means "not measured" — it is never a silent 0% that reads like a measured result.
The metrics
Mention rate
Mention rate is the share of answered runs in which your brand was named.
mention_rate = mentioned_count / answered_count
An answer (or run) is one execution of one prompt against one model in one location on one day. answered_count counts the non-empty answers; runs that returned nothing are excluded, so an outage does not count against you.
Mention rate is answer grain: an answer that names you three times still counts once. If you want occurrences, use mention_total.
Share of voice
Share of Voice is your share of every brand mention the models made. It is instance grain: it counts every occurrence of a brand name, not answers, so a brand named twice in one answer counts twice.
share_of_voice = mention_total / brand_mention_total
brand_mention_total is mention instances across every brand the models named — your tracked competitors, brands you have never heard of, all of them. This is the same Share of Voice the Senso app shows on your dashboard.
Every window figure for Share of Voice — on summary, mentions and prompts — divides by brand_mention_total, so it means the same thing on every endpoint. Two figures are built differently: a prompt row's latest.share_of_voice (below), and each stored answer's own sov_pct on /answers/latest, which is on a 0–100 scale rather than 0–1.
totals also carries tracked_mention_total — mention instances across you plus your tracked competitors. It is not the Share of Voice denominator. It depends on your configuration: add a competitor and it grows, and with no tracked competitors it equals mention_total. brand_mention_total does not move when you edit your competitor list.
To compare yourself only against the competitors you track, compute it yourself:
head_to_head = mention_total / tracked_mention_total # your own math, not a Senso metric
Do not call that result Share of Voice, and do not compare it with the number on the dashboard.
This API does not enumerate the untracked brands by name. They exist only inside brand_mention_total.
Two share-of-voice figures on a prompt row
Rows from /org/analytics/prompts carry share of voice twice. The two are not redundant — they answer different questions over different spans:
| Field | Question it answers | Built from |
|---|---|---|
share_of_voice | Across the whole window, what was your share of brand mentions for this prompt? | every answer in the window |
latest.share_of_voice | In the most recent answers, what is your share of brand mentions for this prompt? | the average share of voice across the most recently collected answer from each model |
latest.share_of_voice. Comparing the window aggregate will disagree, and it will be right to disagree, because it measures a different span.Use the window figure for trends and for ranking prompts against each other: it is built from many answers, so one unusual run moves it far less than it moves the latest figure.
The same distinction applies to the rest of thelatestblock.latest.answer_count,latest.mentioned_countandlatest.modelsdescribe the most recently collected answers, not the window. Those answers can be older than the window, and can come from a model that has no data in it.
Average rank
avg_rank = rank_sum / mentioned_count
The mean ordinal position of your brand across the answers that mentioned it — 1 means named first. Lower is better, and it ships as { "value": 2.4, "display": "#2.4" } so nobody reads it as a percentage.
avg_rank is null when you were never mentioned. That is "never ranked", not "ranked last".
Sentiment
Sentiment is three counts: how many answers that named you were positive, neutral or negative. They add up to mentioned_count.
Citation tiers
Every cited URL is classified into exactly one tier:
| Tier (stored value) | Product name | Meaning |
|---|---|---|
primary | Owned | A page on one of your own websites' domains |
tracked | Tracked | A URL matching one of your configured tracked sources |
secondary | External | Any other, third-party domain |
"External" means the secondary tier only: third-party domains. Tracked sources are their own tier.The two denominators: D and S
Every citation metric divides by one of exactly two denominators:
D = cited_run_count answers with at least 1 citation (any tier) → Rate, Coverage
S = cited_total citation instances (every cited URL) → Share
D is answer grain: an answer citing five of your pages contributes 1. S is instance grain: that same answer contributes 5. A metric built on D and a metric built on S are never comparable — never average them, never sum them, never chart them on a shared axis without labels.
/summary and /citations return both in totals, and the citation-source endpoints in denominators, so you can check which one a number used.
Family 1 — Citation Rate (answer grain, per tier)
Of the answers that cited anything, what share cited this tier?
primary_citation_rate = primary_cited_run_count / D
tracked_citation_rate = tracked_cited_run_count / D
external_citation_rate = external_cited_run_count / D
The three rates are independent, not a partition. An answer citing both an owned page and a third-party page counts toward both numerators, so the three can sum past 100%. That is expected and correct.
Family 2 — Citation Coverage (answer grain, per source)
Of the answers that cited anything, what share cited THIS domain or THIS page?
citation_coverage(source) = (distinct answers citing this source) / D
Citation Coverage is Citation Rate's per-source sibling: same denominator D, narrower numerator. It appears on /citations/domains and /citations/pages. Because the numerator counts distinct answers, coverage is always at most 100%.
A tier rate is not the sum of its sources' coverages — two sources cited in the same answer would double-count it.
Family 3 — Citation Share (instance grain, per tier or per source)
Of all the citation instances we saw, what fraction went to this tier or this source?
primary_citation_share = primary_cited_total / S
tracked_citation_share = tracked_cited_total / S
external_citation_share = external_cited_total / S
citation_share(source) = (citation instances for this source) / S
The three tier shares do partition citations and sum to exactly 100%.
Per-source shares also sum to 100% across all sources in the window — not across the page of results you happen to be looking at.
Rate and Share disagree, and that is the insight
Rate and Coverage count distinct answers. Share counts individual cited URLs. Take a 30-day window:
| Count | Value |
|---|---|
cited_run_count (D) | 100 |
primary_cited_run_count | 40 |
cited_total (S) | 800 |
primary_cited_total | 60 |
| Metric | Math | Result |
|---|---|---|
primary_citation_rate | 40 / 100 | 40.0% |
primary_citation_share | 60 / 800 | 7.5% |
citations_per_answer | 800 / 100 | 8.00 |
High rate with low share means: you get cited, but you are outnumbered inside every answer. That is a content-volume problem, not a discoverability problem. Reversing them — low rate, high share — means the opposite: when you show up you dominate the answer, but you rarely show up.
Citations per answer
citations_per_answer = S / D
An intensity, not a rate: how densely answers cite. It is unbounded above and never carries a percent sign. 4.2 means the average citing answer cited 4.2 URLs.
Counts ship with every ratio
Every payload carries the additive raw counts — numerators and denominators — next to the computed ratio, so:
- You can check our math. Every ratio in
metricsis derivable from the numbers intotals. - You can recombine windows. Counts are additive; ratios are not. Sum the counts across periods, then divide once.
- You can build a metric we did not ship. If you want "answers that cited you as a share of all answered runs", divide
primary_cited_run_countbyanswered_countyourself — just do not call it a Citation Rate, because it is not onD.
/summary response's totals:mention_rate = mentioned_count / answered_count
primary_citation_rate = primary_cited_run_count / cited_run_count (D)
primary_citation_share = primary_cited_total / cited_total (S)
Guard each division: any of those denominators can be zero, which is exactly why the API returns null rather than a number.
Notes, definitions, and data quality
These endpoints are built for agents acting for a human as much as for developers reading a spec, so every response explains itself.
notes[] is an array of prose caveats generated for that response — not boilerplate. They tell you things the numbers cannot, for example:
- that the window defaulted to the 30 days ending on your most recent day with data, rather than today
- that
D = 0, so every citation metric isnullrather than0% - that part of your window is older than the 396 days of data Senso keeps
- that no brand mentions were recorded at all, so
share_of_voiceisnullrather than0%
definitions{} maps the main metrics and terms in that response to one-line definitions, naming the denominator. They are the same definitions GET /org/analytics/glossary returns.data_quality is a deterministic flag from the answered-run count over the window: low below 100, medium below 400, high otherwise. It ships with answered_count and a reasons[] array. Low does not mean the number is wrong — it means a single run moves it a lot.
If you are surfacing these numbers through an agent or an LLM, pass notes[] and definitions{} through to the model and let it surface them. They exist precisely so a summary does not invent a denominator.
Answer text: latest only
latest_answers and GET /org/analytics/answers/latest return the single most recent answer per prompt, model, and location — with the full response text, its citations, and its competitor mentions.
Historical answer text is not retained. History exists only as the daily additive counts in the series endpoints. So:
- Do not treat the latest answers as a sample of your window. Each is the most recently collected answer for its combination, which for a model or location you no longer run can be months old. Check its
run_at. - Each answer carries its own
run_at, and those can differ from row to row. - Each answer's
citationslist holds at most 50 entries. competitor_mentionsmaps each tracked competitor named in the answer to how often it was named, such as{"Profound": 1}. Untracked brands are counted inbrand_mention_totalbut are never enumerated by name.
from and to on /answers/latest filter collection time, not history
GET /org/analytics/answers/latest accepts from and to (YYYY-MM-DD, same validation as everywhere else: from on or before to, and to at most 365 days after it). They filter on run_at — when each stored answer was collected. Both are optional, and unlike the window-scoped endpoints there is no 30-day default here: omit them and you get every stored latest answer.
They do not turn this endpoint into a historical feed. It still returns only the newest answer per prompt, model, and location, so narrowing the window hides the combinations whose latest answer falls outside it. It never returns an older answer in place of the newest one, because older answer text is not stored.
Read it as "show me only the combinations refreshed in this period", never as "show me what the models said in this period".
A window in which none of the latest answers were collected returns an empty list, not the whole snapshot.
curl -G "https://apiv2.senso.ai/api/v1/org/analytics/answers/latest" \
-H "X-API-Key: $SENSO_API_KEY" \
--data-urlencode "from=2026-09-22" \
--data-urlencode "to=2026-09-28"For what the models said over a period, use /org/analytics/mentions or /org/analytics/citations — those read the daily counts and are genuinely historical.
The window
Window-scoped endpoints take from and to as YYYY-MM-DD, both optional, with to at most 365 days after from.
Leave them off and the window defaults to the 30 days ending on the most recent day that has data for your active model and location filter — not today. This matters: monitoring runs on a schedule, so anchoring on today would make every unparameterized request look empty between runs. The day used is echoed back as window.latest_data_day, and the defaulting is called out in notes[].
Senso keeps 396 days of daily data. A window reaching further back covers only the days still kept, and notes[] says so.
/answers/latest takes the same two parameters with the same validation, but they mean something different there — they filter on each answer's run_at and cannot return history. See from and to on /answers/latest above.
Endpoint reference
All paths are relative to https://apiv2.senso.ai/api/v1 and all are GET.
| Path | What it answers | Key parameters |
|---|---|---|
/org/analytics/glossary | What does each metric mean, and what is its denominator? | none |
/org/analytics/filters | Which models, locations, prompt types, tags and competitors actually have data? | none |
/org/analytics/summary | Where do we stand, and how does that compare with the previous equal-length window? | from, to, models, location, prompt_type, tag |
/org/analytics/mentions | How has visibility moved over time — mentions, share of voice, rank, sentiment? | shared filters, group_by (day or week) |
/org/analytics/citations | How has citation performance moved over time, at both grains and in both metric families? | shared filters, group_by |
/org/analytics/citations/domains | Which domains do the models cite, and how much of that is ours? | shared filters, tier, domain_contains, sort, limit, offset |
/org/analytics/citations/pages | Which exact URLs get cited, and which prompts drive them? | shared filters, tier, domain, domain_contains, url_contains, sort, limit, offset |
/org/analytics/prompts | Which prompts are we winning, and which are we invisible on? | shared filters, search, sort, order, limit, offset |
/org/analytics/prompts/{promptId} | One prompt end to end: its metric history plus its latest full answers | shared filters, include_answers |
/org/analytics/answers/latest | What did the models actually say, verbatim? | from, to (on run_at), models, location, prompt_type, tag, mentioned, cited, citation_tier, limit, offset |
| Parameter | Values | Notes |
|---|---|---|
from, to | YYYY-MM-DD | Both optional; from on or before to, and to at most 365 days after it |
models | comma-separated model ids | Use the ids from /org/analytics/filters, exactly as listed. An id Senso does not recognize is a 400; a recognized model with no data for you returns empty results |
location or locations | comma-separated locations | Exact match and case sensitive. Both spellings are the same filter |
prompt_type | awareness, consideration, evaluation, decision | The funnel stages, in any case. Any other value is a 400 |
tag | tag name | Not for the two citation-source endpoints |
us/california does not match US/California, and US does not include US/California. A location or tag with no data returns empty results, not an error. Use /org/analytics/filters to see the exact codes and tags your organization has data for.Which filters each endpoint takes
| Endpoint | models | location(s) | prompt_type | from/to | tag |
|---|---|---|---|---|---|
/glossary, /filters | — | — | — | — | — |
/summary | yes | yes | yes | yes | yes |
/mentions | yes | yes | yes | yes | yes |
/citations | yes | yes | yes | yes | yes |
/citations/domains | yes | yes | yes | yes | no |
/citations/pages | yes | yes | yes | yes | no |
/prompts | yes | yes | yes | yes | yes |
/prompts/{promptId} | yes | yes | yes | yes | yes |
/answers/latest | yes | yes | yes | yes, on run_at | yes |
tagis not a filter on the citation-source endpoints./citations/domainsand/citations/pagesalways cover all your prompts, and atagpassed to them is ignored. For the citation picture of a tag's prompts, use/citationsor/prompts./glossaryand/filterstake no filters at all. They describe the vocabulary and the available filter values; they report no metrics.prompt_typeandtagare redundant on/prompts/{promptId}. The path already names one prompt, so a value that does not match it only empties its totals and series. The prompt'slatest_answersare filtered bymodelsandlocationonly. They are included by default;include_answers=falsereturnslatest_answersas an empty list.
group_by=week, each period starts on a Monday, so the first and last weeks of a window can be partial; every period carries its own counts. A day or week with no data is left out of the series rather than returned as zero.tier on the citation-source endpoints, and citation_tier on /answers/latest, take the stored tier values: primary, tracked or secondary.
Paging defaults to limit=50 (limit=25 on /answers/latest) and is capped at 100. Sorting on the cited-source endpoints is sort=citations (default, by citation instances) or sort=coverage; on /prompts it is mention_rate (default), share_of_voice, citations (answers that cited anything), answered, or text, with order=asc or order=desc (default).
Examples
Read the glossary once
Have your agent call this before it quotes any number, so it never invents a denominator.
curl "https://apiv2.senso.ai/api/v1/org/analytics/glossary" \
-H "X-API-Key: $SENSO_API_KEY"Each entry carries a metric and its definition, plus its denominator and a gotcha where they apply:
{
"entries": [
{
"metric": "primary_citation_rate",
"definition": "Of the answers that cited anything, the share that cited one of your own domains (tier: primary / Owned).",
"denominator": "D = cited_run_count",
"gotcha": "The three tier rates are independent, not a partition — an answer citing both an owned and an external page counts toward both, so they can sum past 100%. That is expected."
}
]
}The one-call dashboard
The response below is illustrative: its numbers are the worked example from earlier on this page.
curl -G "https://apiv2.senso.ai/api/v1/org/analytics/summary" \
-H "X-API-Key: $SENSO_API_KEY" \
--data-urlencode "from=2026-06-01" \
--data-urlencode "to=2026-06-30" \
--data-urlencode "models=chatgpt,perplexity"{
"window": { "from": "2026-06-01", "to": "2026-06-30", "days": 30, "latest_data_day": "2026-10-01" },
"totals": {
"run_count": 1240, "answered_count": 1198,
"mentioned_count": 431, "mention_total": 602,
"tracked_mention_total": 2410, "brand_mention_total": 5180,
"cited_run_count": 100, "primary_cited_run_count": 40,
"cited_total": 800, "primary_cited_total": 60
},
"metrics": {
"mention_rate": { "value": 0.3597, "display": "36.0%" },
"share_of_voice": { "value": 0.1162, "display": "11.6%" },
"avg_rank": { "value": 2.4, "display": "#2.4" },
"primary_citation_rate": { "value": 0.40, "display": "40.0%" },
"primary_citation_share": { "value": 0.075, "display": "7.5%" },
"citations_per_answer": { "value": 8.0, "display": "8.00 citations per cited answer" }
},
"previous_window": { "window": { "from": "2026-05-02", "to": "2026-05-31", "days": 30, "latest_data_day": "2026-10-01" }, "totals": { "...": "..." }, "metrics": { "...": "..." } },
"deltas": { "mention_rate": { "delta": 0.041, "prev": 0.3187, "direction": "improved", "display": "+4.1 pts" } },
"data_quality": { "level": "high", "answered_count": 1198, "reasons": [] },
"notes": ["..."],
"definitions": { "mention_rate": "Share of answered runs in which your brand was named." }
}latest_data_day is the most recent day with data for your model and location filter, whichever window you ask for, and null if there is none. previous_window is the equal-length window immediately before yours, with its own totals and metrics. deltas compares the two for mention_rate, share_of_voice and primary_citation_rate: delta is the difference between the two values, prev is the earlier value, display shows the difference in absolute percentage points, and direction is improved, declined or flat. A delta is null when either window lacked the denominator to compute the metric, so "no data" never reads as "fell to zero".
Find the pages the models cite instead of yours
curl -G "https://apiv2.senso.ai/api/v1/org/analytics/citations/pages" \
-H "X-API-Key: $SENSO_API_KEY" \
--data-urlencode "tier=secondary" \
--data-urlencode "sort=coverage" \
--data-urlencode "limit=10"tier=secondary keeps third-party pages only, and sort=coverage ranks them by Citation Coverage rather than raw volume. Each page carries its citation_coverage, and denominators.cited_run_count is the D it divides by. top_prompts lists at most five prompts per page — the biggest drivers, not an exhaustive list. A cited URL may not match your published link character for character.
Find the prompts where you are invisible
curl -G "https://apiv2.senso.ai/api/v1/org/analytics/prompts" \
-H "X-API-Key: $SENSO_API_KEY" \
--data-urlencode "sort=mention_rate" \
--data-urlencode "order=asc" \
--data-urlencode "limit=20"Each row carries that prompt's own counts, so read mention_rate alongside answered_count — a prompt answered twice and a prompt answered 200 times both report a rate. A prompt with no answered runs in the window is not listed at all, so this list cannot show you prompts that were never measured. Compare it with your prompts in GET /org/prompts to find those.
Read what a model actually said
curl -G "https://apiv2.senso.ai/api/v1/org/analytics/answers/latest" \
-H "X-API-Key: $SENSO_API_KEY" \
--data-urlencode "mentioned=false" \
--data-urlencode "cited=true" \
--data-urlencode "limit=5"That combination — answers that cited sources but never named you — is usually the most actionable feed in the API.
Common mistakes
Dividing by the wrong denominator. primary_cited_run_count / answered_count is not a Citation Rate. Citation Rate and Citation Coverage divide by D (cited_run_count); Citation Share divides by S (cited_total). Mixing an answer-grain numerator with an instance-grain denominator produces a number that can exceed 100% for no interpretable reason. Both denominators are in every response — use them.
Treating null as zero. A null ratio means the denominator was zero: no answered runs, no brand mentions, no citations at all. Charting it as 0% turns "we did not measure this" into "we measured a failure". The same applies to avg_rank: null is "never mentioned", not "ranked last".
Averaging per-period rates. The series points carry different denominators, so the mean of 30 daily mention rates is not the 30-day mention rate. Sum the counts across periods, then divide once. This is exactly why every series point ships its counts.
Reading share_of_voice as a head-to-head number. Its denominator is brand_mention_total — every brand the models named, tracked or not — so it is your share of the whole field, not of a race against your configured competitors. It is comparable across organizations and across time, and it does not move when you edit your competitor list. If you divide by tracked_mention_total instead, that number is neither comparable across organizations nor what the Senso app shows.
Reading "external" as "not owned". In these endpoints external is the secondary tier only — third-party domains. Tracked sources are their own tier.
Summing the three tier rates and expecting 100%. The rates are independent and can total more than 100%. It is the three tier shares that partition citations and sum to exactly 100%.
