Skip to content
unzoi docs
Search and navigation
Start here
REST API
MCP
Limits and plans
Agent clients
SDKs
Guides

Evaluating coverage

"Was this covered?" is a different question from "what happened?", and it is answered with counts rather than with articles.

Volume is not attention

A search returning 400 articles tells you almost nothing. It could be 400 outlets independently covering something, or it could be one press release republished 400 times. /stories separates them:

curl -s -G "https://api.unzoi.com/stories" -H "x-api-key: $UNZOI_KEY" \
  --data-urlencode "q=battery plant announcement" \
  --data-urlencode "from=2026-08-01" \
  --data-urlencode "limit=20" \
  | jq -r '.stories[] | "\(.outlets)\t\(.count)\t\(.title)"'
outletscountReading
3541Widely distributed. Check sources and title_variants before calling it independent.
340One wire story or press release, syndicated. High volume, no independent interest.
22Early or niche. Check again tomorrow before drawing a conclusion.

The outlets/count ratio is the most useful single number this API gives you for judging attention, and a plain article search discards it entirely. It measures distribution, not newsrooms: outlets counts distinct publisher domains, and a wire service or an ownership group can put one newsroom's report on many of them. Read sources — the domains themselves — before calling 35 outlets independent coverage, and title_variants on /stories/{id}: 35 outlets carrying two distinct headlines is one or two reports, redistributed.

Who covered it

Facets count over the whole match set, not the page — so one request tells you the shape of the coverage:

curl -s -G "https://api.unzoi.com/search" -H "x-api-key: $UNZOI_KEY" \
  --data-urlencode "q=battery plant announcement" \
  --data-urlencode "from=2026-08-01" \
  --data-urlencode "facets=source,publisher_country,language" \
  --data-urlencode "facet_limit=20" \
  --data-urlencode "limit=1" \
  | jq '.facets | map_values([.values[] | "\(.value): \(.count)"])'

limit=1 because you want the counts, not the articles. One billed request either way, and this way you are not paying context for results you will discard.

Coverage over time

/aggregate answers it in one request: the same query and filters, cut into calendar days, with articles, distinct stories and distinct outlets counted in each.

curl -s -G "https://api.unzoi.com/aggregate" -H "x-api-key: $UNZOI_KEY" \
  --data-urlencode "q=port congestion" \
  --data-urlencode "from=$(date -u -d '13 days ago' +%Y-%m-%d)" \
  --data-urlencode "interval=day" \
  --data-urlencode "metrics=stories,outlets" \
  | jq -r '.buckets[] | "\(.from[0:8])  \([range(.stories) | "#"] | join(""))"'

Fourteen buckets, one request. Plot stories rather than articles: an article series inflates the same event, and it is events you are plotting. Check approximate on a bucket before quoting its figure: a distinct count summed across index partitions can count one story twice. When a count is approximate.

The portable way, which needs nothing but /stories, is one request per window:

for day in $(seq 13 -1 0); do
  d=$(date -u -d "$day days ago" +%Y-%m-%d)
  n=$(curl -s -G "https://api.unzoi.com/stories" -H "x-api-key: $UNZOI_KEY" \
    --data-urlencode "q=port congestion" \
    --data-urlencode "from=$d" --data-urlencode "to=$d" \
    --data-urlencode "limit=1" | jq -r '.total')
  printf '%s  %s\n' "$d" "$(printf '%*s' "$n" '' | tr ' ' '#')"
done

Fourteen requests for the same fortnight, each small enough to be fast and likely to succeed. Worth keeping for a client that already speaks /stories, and as a cross-check when a bucket says approximate.

Did the framing differ?

This is the question clustering cannot answer — you need the articles, split by who published them. /stories/{id} gives the story's languages and publisher countries as counts in one request; when you want the articles behind them, expand the cluster and facet it:

# The articles in one story, by publisher country.
curl -s -G "https://api.unzoi.com/search" -H "x-api-key: $UNZOI_KEY" \
  --data-urlencode "story_id=s-9f2c" \
  --data-urlencode "facets=publisher_country,language" \
  --data-urlencode "limit=50"

Then compare titles across the groups. The signal values on each article give you a second axis — the same event covered with markedly different financial_uncertainty or conflict intensity is a framing difference you can point at rather than assert.

What you cannot conclude

  • Absence of coverage, from an incomplete answer. A response with partial: true or a coverage other than complete did not read the whole index, and a short result under it is not a small story. Zero results may also be a wrong filter value (resolve it with /entities), a plan-clamped window, or a language you did not include. Check all three fields, and re-run without filters, before saying "nobody covered this" — Reading completeness.
  • Reach. outlets counts publishers, not readers. A story in three major nationals may reach more people than one in thirty local sites.
  • Sentiment. Signals measure register, not approval. A high financial_negative means the language is that of financial distress, not that the article disapproves.
  • Totals from a ranked query. total_relation: approximate means the number is a candidate count. Use a keyword query if the figure needs to be defensible.

A repeatable check

# One question, three numbers and a yes/no: events, articles, distinct outlets,
# and whether the index actually answered.
q="grid outage"; from="2026-08-01"

stories=$(curl -s -G "https://api.unzoi.com/stories" -H "x-api-key: $UNZOI_KEY" \
  --data-urlencode "q=$q" --data-urlencode "from=$from" --data-urlencode "limit=100")

echo "events:   $(echo "$stories" | jq -r '.total')"
echo "articles: $(echo "$stories" | jq '[.stories[].count] | add')"
echo "outlets:  $(echo "$stories" | jq '[.stories[].sources[]] | unique | length')"
echo "complete: $(echo "$stories" | jq '(.partial | not) and .coverage == "complete" and (.from_clamped | not)')"

Three numbers and a boolean, one request. Enough to say whether something was covered and how widely — and, when complete is false, that the other three are a lower bound rather than an answer.