Senso
Sign inSign up

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.

Your prompts run against AI models on a schedule. The analytics endpoints report what those answers said about you — how often you were named, who was named alongside you, and which pages the models cited.

Every endpoint under /org/analytics is read-only, org-scoped, and authenticated with your org API key (X-API-Key). The org is always taken from the key, never from a path or query parameter, so there is no cross-org read to get wrong.

curl "https://apiv2.senso.ai/api/v1/org/analytics/summary" \
  -H "X-API-Key: $SENSO_API_KEY"

---

The shape of every response

Aggregate endpoints return the same five blocks, in the same order:

BlockWhat it holds
windowThe effective date range: from, to, days, and latest_data_day
totalsEvery raw count — all numerators and all denominators
metricsEvery ratio derived from totals, each as { value, display }
data_qualityA low / medium / high confidence flag from the answered-run count
notes + definitionsProse caveats and one-line metric definitions, generated per response
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 reports on your dashboard, and the two are expected to match exactly.

There is one Share of Voice metric and one denominator. Every endpoint that reports it — summary, mentions, prompts — computes it the same way, so the number never changes meaning as you move between them. Prompt rows additionally carry a latest-answer figure alongside the window one; see below.

totals also carries tracked_mention_total — mention instances across you plus your tracked competitors. It is deliberately not the Share of Voice denominator. For an organization with no tracked competitors configured, tracked_mention_total equals mention_total, so dividing by it reports 100% whenever you were mentioned at all. Reconciled against production data in July 2026, three of four sampled organizations tracked no competitors, so that denominator would have returned 100.0% where the app showed 37 to 40 percent.

Even when it is not degenerate, that denominator depends on your configuration: add a competitor and it grows, so the number falls without anything changing in the answers. brand_mention_total does not move when you edit your competitor list.

The raw count is still returned, so you can compute the head-to-head view 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:

FieldQuestion it answersBuilt from
share_of_voiceAcross the whole window, what was your share of brand mentions for this prompt?every answer in the window
latest.share_of_voiceRight now, what is your share of brand mentions for this prompt?the newest answer per model and location
The Senso app's prompt table shows the latest figure. So when you are reconciling a single prompt against the app, compare 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 the latest block. latest.answer_count and latest.mentioned_count describe the newest answers only, not the window.

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

Counts only: positive + neutral + negative = mentioned_count. There is deliberately no averaged sentiment score — averaging categorical labels invents a number.

Citation tiers

Every cited URL is classified into exactly one tier by its domain:

Tier (stored value)Product nameMeaning
primaryOwnedOne of your own websites
trackedTrackedOne of your configured tracked sources
secondaryExternalAny other, third-party domain
The tiers partition citations — each cited URL is exactly one tier — but they do not partition answers. One answer can carry citations of two or three tiers at once.

"External" means the secondary tier only. The older Partner API uses "external" to mean everything you do not own (tracked and secondary combined). The two are not comparable — an SDK external field and a Partner external field count different things.

The two denominators: D and S

This is the one thing worth reading twice. 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.

Both denominators are returned explicitly on every citation response, so you can always 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 must sum to exactly 100%. If they do not, that is a producer bug, not rounding — and the API says so in notes[] when it detects the mismatch.

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:

CountValue
cited_run_count (D)100
primary_cited_run_count40
cited_total (S)800
primary_cited_total60
MetricMathResult
primary_citation_rate40 / 10040.0%
primary_citation_share60 / 8007.5%
citations_per_answer800 / 1008.00
Both numbers are correct and they describe different things. You were cited in 40% of the answers that cited anything — good visibility. But you were only 7.5% of the citations inside those answers — every answer that cites you cites roughly eight sources, and seven of them are not yours.

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. This is deliberate:

  • You can check our math. Every ratio in metrics is derivable from the numbers in totals.
  • 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_count by answered_count yourself — just do not call it a Citation Rate, because it is not on D.
  • Recomputing the headline metric from a /summary response:

    import os, requests
    
    KEY  = os.environ["SENSO_API_KEY"]
    BASE = "https://apiv2.senso.ai/api/v1"
    HEADERS = {"X-API-Key": KEY}
    
    data = requests.get(f"{BASE}/org/analytics/summary", headers=HEADERS).json()
    t = data["totals"]
    
    mention_rate = t["mentioned_count"] / t["answered_count"]        # ÷ answered_count
    primary_rate = t["primary_cited_run_count"] / t["cited_run_count"]  # ÷ D
    primary_share = t["primary_cited_total"] / t["cited_total"]         # ÷ S
    
    print(f"mention rate  {mention_rate:.1%}  (API: {data['metrics']['mention_rate']['display']})")
    print(f"Citation Rate {primary_rate:.1%}  (API: {data['metrics']['primary_citation_rate']['display']})")
    print(f"Citation Share {primary_share:.1%} (API: {data['metrics']['primary_citation_share']['display']})")
    curl "https://apiv2.senso.ai/api/v1/org/analytics/summary" \
      -H "X-API-Key: $SENSO_API_KEY"

    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 is null rather than 0%
  • that part of your window predates the 396-day rollup retention limit and was pruned
  • that the per-tier instance totals do not sum to cited_total, so tier shares in this window are unreliable
  • that no brand mentions were recorded at all, so share_of_voice is null rather than 0%
  • definitions{} maps each metric present in that response to its one-line canonical definition, naming the denominator. It is projected from the same source as GET /org/analytics/glossary, so a definition can never drift between endpoints.

    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 — they are a snapshot of right now.
  • Each answer carries its own run_at, and those can differ from row to row.
  • Each answer's citations list is capped at 50 entries by the rollup pipeline.
  • competitor_mentions covers your tracked competitors only. Untracked brands are counted in brand_mention_total but 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, 365-day maximum span). 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 worked example from a live organization: 2,392 answers with no window, 2,388 for the last four days (the four that had not been re-run recently drop out), and 0 for a window in the year 2000 — an empty result, 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-07-24" \
      --data-urlencode "to=2026-07-28"

    For what the models said over a period, use /org/analytics/mentions or /org/analytics/citations — those read the daily rollups and are genuinely historical.

    ---

    The window

    Window-scoped endpoints take from and to as YYYY-MM-DD, both optional, up to 365 days apart.

    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[].

    Daily rollups are pruned after 396 days, so windows reaching further back are silently truncated by retention.

    /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.

    PathWhat it answersKey parameters
    /org/analytics/glossaryWhat does each metric mean, and what is its denominator?none
    /org/analytics/filtersWhich models, locations, prompt types, tags and competitors actually have data?none
    /org/analytics/summaryWhere do we stand, and how does that compare with the previous equal-length window?from, to, models, location, prompt_type, tag
    /org/analytics/mentionsHow has visibility moved over time — mentions, share of voice, rank, sentiment?shared filters, group_by (day or week)
    /org/analytics/citationsHow has citation performance moved over time, at both grains and in both metric families?shared filters, group_by
    /org/analytics/citations/domainsWhich domains do the models cite, and how much of that is ours?shared filters, tier, domain_contains, sort, limit, offset
    /org/analytics/citations/pagesWhich exact URLs get cited, and which prompts drive them?shared filters, tier, domain, domain_contains, url_contains, sort, limit, offset
    /org/analytics/promptsWhich 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 answersshared filters, include_answers
    /org/analytics/answers/latestWhat did the models actually say, verbatim?from, to (on run_at), models, location, prompt_type, tag, mentioned, cited, citation_tier, limit, offset
    The shared filters accepted by every window-scoped endpoint:

    ParameterValuesNotes
    from, toYYYY-MM-DDBoth optional, 365 days maximum span, from on or before to
    modelscomma-separated model idsUse the ids from /org/analytics/filters
    location or locationscomma-separated locationsExact match, case sensitive, e.g. US, US/California. Both spellings are the same filter
    prompt_typeawareness, consideration, evaluation, decisionThe canonical funnel stages
    tagtag nameDoes not apply to the two citation-source endpoints
    location and locations are the same parameter. The plural is an alias, and it exists for a specific reason: an unrecognized filter name is not an error, it is simply no filter — so a caller who guessed the plural would have received more data than they asked for, silently, with no way to tell. Both names take a comma-separated list of exact location codes. Values are case-sensitive: US/California matches, us/california does not. Use /org/analytics/filters to see the codes your organization actually has data for.

    Which filters each endpoint takes

    Verified against production, endpoint by endpoint:

    Endpointmodelslocation(s)prompt_typefrom/totag
    /glossary, /filters
    /summaryyesyesyesyesyes
    /mentionsyesyesyesyesyes
    /citationsyesyesyesyesyes
    /citations/domainsyesyesyesyesno
    /citations/pagesyesyesyesyesno
    /promptsyesyesyesyesyes
    /prompts/{promptId}yesyesyes
    /answers/latestyesyesyesyes, on run_atyes
    Three things to read off that table:

  • tag does not narrow the citation-source endpoints. The domain and webpage rollups have no prompt grain, so there is nothing for a prompt tag to match. Passing tag to /citations/domains or /citations/pages is not an error and not a silent empty result — you get the whole cited-source landscape, covering all prompts. Both endpoints say so in notes[] when you pass a tag. If you need tagged prompts only, get the tag's citation picture from /citations or /prompts, which are prompt grain.
  • /glossary and /filters take no filters at all. They describe the vocabulary and the available filter values; they report no metrics.
  • prompt_type and tag are redundant on /prompts/{promptId}. The path already names one prompt, so they can only exclude it entirely.
  • 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) or sort=coverage; on /prompts it is mention_rate (default), share_of_voice, citations, answered, or text, with order=asc or order=desc.

    ---

    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, its definition, its denominator, and a gotcha:

    {
      "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

    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-06-30" },
      "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 } },
      "deltas": { "mention_rate": { "delta": 0.041, "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." }
    }

    deltas compares against the equal-length window immediately before yours and is expressed in absolute percentage points. 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

    import os, requests
    
    KEY  = os.environ["SENSO_API_KEY"]
    BASE = "https://apiv2.senso.ai/api/v1"
    HEADERS = {"X-API-Key": KEY}
    
    resp = requests.get(f"{BASE}/org/analytics/citations/pages", headers=HEADERS, params={
        "tier": "secondary",     # third-party pages only
        "sort": "coverage",      # rank by Citation Coverage, not raw volume
        "limit": 10,
    })
    data = resp.json()
    d = data["denominators"]["cited_run_count"]
    
    for p in data["pages"]:
        cov = p["citation_coverage"]
        print(f'{cov["display"] if cov else "—":>7}  {p["url"]}')
        for prompt in p["top_prompts"]:
            print(f'          driven by: {prompt["prompt_text"]}')
    print(f"\nCitation Coverage denominator D = {d} answers with at least one citation")
    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"

    top_prompts lists at most five prompts per page — the biggest drivers, not an exhaustive list. URLs are canonical: tracking parameters are stripped and the address normalized when the rollup is written, so they may not match your published links 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. Prompts with no denominator (never answered, or no brand mentions at all) sort to the front of an ascending sort: they are unmeasured, not zero.

    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 orgs nor equal to what the Senso app shows, and it reads 100% for any org that tracks no competitors.

    Reading "external" as "not owned". In these endpoints external is the secondary tier only — third-party domains. Tracked sources are their own tier. The older Partner API uses "external" for tracked and secondary combined, and the two are not comparable.

    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%.