DocsAPI & MCP

Conventions

The rules every response follows. Scales, null handling, note fields, the fixed window, engine blending, and the contract-freeze policy.

Read this once. Then every field on every endpoint reads the same way.

Scales — the fraction rule and its exceptions

Not every number is on the same scale. Rate and share fields are 0–1 fractions. Scores are 0–100. Everything else is a raw count or a rank.

FieldScaleNotes
presence_rate0–1 fractionrounded to 0.001
citation_share0–1 fraction or nullunrounded
share_of_voice_fraction0–1 fractionrounded to 0.001; prefer this over share_of_voice
win_rate, visibility_share (shopping)0–1 fractionrounded to 0.001
own_carousel_share, own_win_share (shopping)0–1 fractionrounded to 0.001
visibility0–100 scoreposition-weighted
sentiment_index0–100 score or null50 = neutral
share_of_voice0–100 percentagedeprecated, frozen legacy outlier; same number as share_of_voice_fraction times 100
avg_position, own_avg_positionrank (mean 1-based) or nulllower is better, not a fraction
run_count, appearances, carousels, citations, window_daysinteger count
Note

share_of_voice is a percentage, on 0 to 100. presence_rate and citation_share are fractions, on 0 to 1. share_of_voice shipped in v1 as the one exception to the fraction rule, so the contract freeze keeps it exactly as it is: deprecated but frozen. The additive share_of_voice_fraction carries the same number on the standard 0 to 1 scale. Use that one in new integrations, and never multiply the wrong one by 100. The shopping fields follow the fraction rule. Every rate and share there is 0 to 1, and the names match the shopping CSV export exactly.

Null is not zero

A field that can be null is null when there is nothing to compute. It is never a placeholder 0:

  • citation_share is null when no run in the window carried any citations.
  • sentiment_index is null when no scored brand mention exists in the window.
  • avg_position is null when the entity never appeared.
  • own_avg_position (shopping) is null on days your product wasn't on the shelf.
  • Optional shopping strings (merchant, merchant_domain, price, url) are null when the engine's payload carried none. A merchant domain only ever comes from the product link. We never guess it.

A 0 in a response is a measured zero. Do the same in your integration: treat null as "unknown", never as "zero". See Data honesty.

note fields

The shopping endpoints carry a note field. It is a readable string, or null:

  • On /api/v1/shopping, note explains an empty result, e.g. "No shopping carousels were captured for this brand's tracked prompts in this window." (totals.carousels will be 0). Report that as absence of data, never as shopping presence.
  • On /api/v1/shopping-trend, with a single collected day, note says the data is a snapshot, not a trend. Check days_observed before you chart a line.

The window

The read views cover a fixed rolling 7-day window, anchored at the brand's newest collected day. Every read response includes window_days (currently 7). The read API has no pagination or date-range parameters. The data behind it is cached with a one-hour TTL.

Engine blending

Headline metrics (/overview, /prompts, /competitors, /sources) blend only the search-grounded engines, so the numbers keep one meaning. /answers is the exception. It returns runs from all engines, unfiltered.

The ten engines are: chatgpt, perplexity, gemini, aio, aimode, claude, copilot, grok, deepseek, mistral. The last two, DeepSeek and Mistral, are knowledge-only. See Metrics overview.

Contract-freeze policy

The v1 surface is stable:

  • Released fields never change meaning. A field's name and meaning freeze once it ships. New things arrive as additive fields. Topic folders shipped as topic_id and topic_name next to the frozen topic, which stays the first tag. Code that keys on topic never sees it relabel.
  • Metric math is versioned separately (METRICS_VERSION). A change to the computation bumps that version instead of silently altering a field.
  • No existence leaks. A brand id outside your scope returns the same "unknown brand" response as one that does not exist.