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

Synced from https://api.unzoi.com/docs/changelog.md. This site describes version 0.7.0 of the contract.

Changelog

All notable changes to the public API and MCP server. The format follows Keep a Changelog; versions are the api crate’s, which is also info.version in /openapi.json.

[0.7.0] - unreleased

Credits: every call is priced from one published table, checked and reserved before it runs, and charged the same on every surface.

Breaking

  • request_units is credits_charged, and x-request-units is x-credits-charged.
  • x-quota-limit / -used / -remaining / -reset are x-credits-limit / -used / -remaining / -reset.
  • x-ratelimit-limit / -remaining / -reset are x-credits-per-minute / -per-minute-remaining / -per-minute-reset, and count credits.
  • Error code quota_exceeded is insufficient_credits, and rate_limited is credit_rate_limited, or message_rate_limited for free calls: MCP messages, previews, /account, and requests refused before they ran. Refusal bodies carry the call’s credits range and the figures that refused it.
  • /account and account_status report credits_limit, credits_used, credits_remaining, credits_per_minute, credits_reset_secs and credits_charged (0); account_status adds credits_this_session. The control API’s tenant adds calls_this_period.
  • Plans report monthly_credits, credits_per_minute and overage_cents_per_1k_credits; /v1/plans adds credit_costs.
  • The control API’s usage reports credits and calls per day and per operation.
  • The Stripe billing meter is credits.
  • Watch status paused_quota is paused_credits.
  • /stories refuses an offset above 1,000.

Added

  • estimate=true on every endpoint and tool returns the call’s credit range, a breakdown and whether it would run, without running it, for 0 credits.
  • max_credits on every endpoint and tool refuses a call whose worst case costs more (credit_budget_exceeded, 400), at no cost.
  • Every tool description and every endpoint in /openapi.json ends with its price; /docs/credits.md has the table.
  • Webhook delivery status held_credits: a hard-capped key that runs out holds its pending deliveries, retried hourly, until credits return; a held event that reaches its seven-day retention fails.
  • A create preview (POST /watches?estimate=true, create_watch with estimate) adds per_run, runs_per_month and webhook_attempts_per_event_max.
  • Query analytics events record credits, and the rollups carry a credits column (rollup format 2).

Changed

  • A hard-capped key is refused before a call whose worst case would not fit, rather than overshooting its allocation.
  • The per-minute limit counts credits. A call larger than a minute’s credits waits for a full minute instead of being refused.
  • Semantic ranking costs 2, hybrid 3, find_related 2. Offsets cost 1 per full 1,000 rows on /search and /top-headlines and 1 per full 100 on /stories.
  • A watch run costs the credits its query takes, not 1. Every webhook delivery attempt costs 1, retries included.
  • A story expansion costs 2 on every surface; the gateway charged 1 over REST. The gateway’s percentile pass costs 1, and a network comparison a flat 3.
  • account_status, /account, MCP initialize and tools/list are free, and take a message token instead. A suspended key’s refusal takes one too.
  • What a call is charged, by outcome: a 2xx what it took, held inside its estimate; a 404 the operation’s minimum; a refusal, an error or a call stopped at the request timeout (408), nothing.
  • search_news over MCP defaults to keyword ranking on every host.

[0.6.0] - unreleased

Geographic search, event classes and numeric amounts, from fields derived out of records every GKG document already carries. They reach a shard when its splits are next rebuilt; until then a filter that needs them says so.

Added

  • near=latitude,longitude with radius_km (0.1-2000), or bbox=south,west,north,east, on /search, /stories, /top-headlines and their tools: articles mentioning a city in the area. The index narrows to the geohash cells covering it and every mentioned city is then checked exactly. Hits carry matched_locations (name, country, coordinates and, for near, distance_km), nearest first. Cities only: region and country coordinates are centroids. A box whose west is greater than its east crosses the antimeridian; an area needing more than 256 cells is refused.
  • event_type on the same endpoints and /aggregate, a facet of the same name, and event_types on every article: seventeen classes (events-v1) derived from GKG themes, among them bankruptcy, earnings_report, ipo, cyber_attack, outage, executive_change, labor_strike and supply_shortage. Each class names the themes it needs, checked against GDELT’s theme vocabulary; where a theme alone is ambiguous a second theme is required (an appointment is an executive_change only with a chief executive). Acquisitions, funding rounds, layoffs, lawsuits, contract awards, product launches and factory openings have no reliable theme and are not classes.
  • amount_min and amount_max: articles mentioning a numeric amount within the bounds. One amount must satisfy every bound, and with amount_object that amount must describe the object. amounts[].number on /doc/{id} is the value compared.
  • coverage: field_unavailable: a filter needs a field part of the index was built without. The answer is partial and covers only the rebuilt part; the gateway keeps it through the merge.

Changed

  • A geographic or amount filter on a keyword or browse query reads candidates in batches of 500 (up to 2,000) and checks each one; request_units counts the batches. When the candidates outnumber the scan, total is estimated and total_relation is approximate, and facets, which count candidates, are marked approximate.

Fixed

  • x-request-units is documented among the response headers in /openapi.json; it was sent but not listed.
  • A graph edge’s share is measured from the entity the edge is seen from: the starting entity, or the end entity on a path’s second hop, which used to divide its co-mentions by themselves. A one-hop connection_path counts the starting entity, so its direct edge’s share is no longer 0.
  • A graph edge’s type lists the seven entity fields neighbours come from, not all nine.
  • account_status costs one request unit, as every MCP request does; its description called it free.
  • A geographic or amount filter serves matches only from the candidates it checked, and has_more is false once those run out instead of promising a page it cannot serve. cursor cannot be combined with these filters.
  • The gateway keeps a shard’s approximate total, so a scan that ran out of candidates is not reported as exact.
  • A filter on a derived field costs one extra request unit wherever the check for the field runs: /stories, semantic and hybrid /search, /aggregate.
  • amount_min and amount_max refuse negative values, as documented.

[0.5.0] - unreleased

Relationships answered at query time, entity ids as filters, and calls metered by the index queries they take.

Added

  • entity_id on /search, /stories, /top-headlines, /aggregate and their tools: up to five ids from /entities (comma separated, or a repeated parameter over REST). Each matches every spelling in its alias group within the request window, and several ids must all match. The response’s resolved lists the spellings each id used; an id with none in the window matches nothing and says so with an empty aliases_used.
  • GET /graph/related and related_entities: the organizations, people, places and topics co-mentioned with one entity, strongest first, each with co_mentions, share, and for the strongest (evidence, default 3) the newest shared articles with distinct sources, stories, first_seen and last_seen.
  • GET /graph/path and connection_path: the direct co-mentions of two entities and up to five paths through one shared neighbour (max_hops 1 or 2).
  • GET /graph/network and entity_network: neighbours grouped by type, and with compare_from/compare_to the neighbours that are new or gone and the ties whose share grew by half or fell by a third.
  • GET /graph/exposures and shared_exposures: what up to five entities have in common, as shared articles, stories, sources, the requested facets and signal statistics (signal_stats), with the newest shared articles as evidence.
  • Every graph answer carries note (co-mention in reporting is association, not ownership, partnership, causation or exposure), indexed_through, entity_resolution_version, request_units, partial and coverage. The corpus can be scoped by language, publisher_country, source_type and country. Bounds: two hops, 50 neighbours (20 per type on a network), 10 evidence articles and 16 index queries per index.
  • request_units in search, story, entity, aggregate and graph responses, and the x-request-units header on every authenticated REST response. A call is charged the index queries it issued: a search costs 1; resolving an entity_id, a signal_percentile_min threshold, any cursor page, a story expansion and every graph operation cost more and say so. A document lookup costs 1 and reports it in the header only. Across indexes a call is charged what the most expensive index needed, and a network comparison both of its passes.
  • get_watch, the MCP tool for GET /watches/{id}.
  • The webhook body is published as the WatchWebhook schema under webhooks in /openapi.json; watch events carry recorded_at.

Changed

  • A watch’s label is label. In 0.4.0 it was name, which is also the proper-name filter a watch’s query accepts, so a labelled watch saved its label as a filter and matched nothing. name in a watch is now that filter.
  • Watch event ids begin with when the run that recorded them was due, so the feed read newest first is the order events were recorded in. In 0.4.0 they began with the story’s newest article time, which let a late-indexed story’s event sort below events an agent had already read.
  • Webhook bodies omit previous_outlets and current_outlets on new_story rather than sending null.
  • Watch routes document 500 (the watch store failed) instead of 503, which they cannot return.

Fixed

  • paused_quota is described as the code behaves: only a key that stops at its quota pauses its watches; a key that bills overage keeps running.

[0.4.0] - unreleased

Watches: the index runs a story query on an interval and records what changed, so a client stops polling.

Added

  • POST /watches, GET /watches, GET /watches/{id}, DELETE /watches/{id}, GET /watches/{id}/events, and the create_watch, list_watches, delete_watch and watch_events tools. A watch is a /stories query (the filters, q, mode and signals it may use), interval_minutes, minimum_outlets (default 2), notify_on (new_story, story_growth) and an optional webhook_url.
  • new_story fires when a story first reaches minimum_outlets; story_growth when it crosses 5, 15, 40 or 100 outlets, or grows by 10 since its last event. Events are kept seven days and page with after.
  • Webhooks: https to public hosts only (loopback, private, link-local, carrier-grade NAT, documentation and IPv4-mapped addresses are refused, at creation and at every delivery, and the connection is pinned to the checked address). Each event is POSTed as JSON with x-unzoi-event, x-unzoi-delivery and x-unzoi-signature: sha256=<HMAC-SHA256 of the body> under the webhook_secret returned once at creation; failed deliveries are retried after 1, 5 and 25 minutes.
  • Each run is charged as one request to the key that created the watch. A run whose answer is partial records what it saw and asks the same window again. A revoked or suspended key pauses its watches (paused_key); a spent allowance pauses them hourly (paused_quota).
  • Plans cap watches and their interval: free 2 at 60 minutes, build 25 at 15, scale 200 at 5, archive 1,000 at 1 (plans.json carries watches and min_watch_interval_minutes). Over the cap is 400 with code: watch_limit.
  • Error codes watch_limit and unsupported (a watch tool on a server without a watch store, such as a shard or the stdio binary).

[0.3.0] - unreleased

Query-time capabilities that need no re-indexing: a time series, ordering and origin for stories, keyset paging, and relative signal thresholds.

Added

  • GET /aggregate and the aggregate_news tool: per calendar bucket (hour, day, week from Monday, month, or all), the article count and optionally distinct stories, distinct outlets and a signal’s mean and percentiles (metrics=stories,outlets,signal:finance, percentile=90). interval is required; from comes from the plan window when omitted; at most 366 buckets. Distinct counts summed across indexes are marked approximate.
  • sort=count|outlets|recency on /stories and list_stories; each story carries published_at of its representative article.
  • GET /stories/{id} reports first_reported (the earliest member in the index), title_variants (distinct headlines among the listed articles) and sources_by_first_seen. These are proxies for origin; nothing in the index marks wire copy.
  • cursor on /search, /top-headlines and their tools: keyset paging over a newest-first browse. Pass start, then each next_cursor. Pages are deterministic and disjoint even when many articles share a timestamp. Not with offset, q or a ranked mode.
  • signal_percentile_min (50-99.9) on /search, /stories, /top-headlines: keep the top slice of a signal over the match set. The resolved value is returned as signal_threshold; across indexes it is a count-weighted mean and marked approximate.

Changed

  • aggregate_news and resolve_entity require exactly what their endpoints require, and the OpenAPI document now marks required query parameters (/entities: q, type; /aggregate: interval).

[0.2.0] - unreleased

The “agent-native” release: the documented agent workflow is expressible over MCP, every tool is typed end to end, and a mistake in a request is refused instead of silently changing the question.

Breaking

  • Unknown query parameters are refused. GET /search?organiztion=asml is now 400 {"code": "unknown_parameter", "detail": "Unknown parameter \"organiztion\". Did you mean \"organization\"?"} on every endpoint. It used to be ignored, which searched everything. Over MCP an undeclared argument is JSON-RPC -32602 with the same hint; every tool now declares additionalProperties: false.
  • Unknown signal names are refused (code: unknown_signal) instead of matching nothing; signal_min/signal_max without signal, an unknown facets field, and a malformed signals clause are invalid_parameter.
  • 401, 402 and 429 bodies are JSON, the documented Error shape, instead of plain text. A missing document (404) now carries a body too.
  • Error gains code (stable; invalid_parameter, unknown_parameter, unknown_signal, not_found, unauthorized, rate_limited, suspended, quota_exceeded, upstream_unavailable, internal) and requires error, code, detail. MCP tool errors carry the same JSON in their text block; protocol errors carry code in error.data.
  • search_news no longer requires q: omit it for a newest-first browse, as GET /search always allowed. list_stories and top_headlines gain source, mode and the enums they lacked; top_headlines no longer injects mode=recent (it is implied).
  • /stories honours mode. It was always keyword-ranked; the default is still keyword.

Added

  • GET /stories/{id} and the get_story tool: one story expanded — counts, outlets, first_seen/last_seen, facets over its members, the representative article, and its newest articles as compact hits.
  • GET /entities and the resolve_entity tool: the spellings the index holds for a name, grouped by a deterministic entity_id (<type>:<normalised key>, er-v1) with aliases, counts and a confidence. Approximate by design; not a registry.
  • story_id on search_news, list_stories and top_headlines, and in the facets enum.
  • view=compact on /search, /top-headlines, /similar/{id} and the matching tools: each hit keeps id, title, url, source, published_at, language, story_id, score. Default stays full.
  • partial and coverage on every search-shaped response, on both tiers and on /similar/{id}; the server instructions and tool descriptions state the rule: never conclude absence when partial is true, coverage is not complete, or from_clamped is true.
  • outputSchema on every MCP tool (the OpenAPI component, inlined), so a schema-aware host can validate structuredContent.
  • mode=recent documented; score and the previously undocumented Article fields documented; ArticleDetail’s quotes, amounts, date_mentions and location_details typed; the signal parameter carries its enum in the OpenAPI document.
  • A parity test: every tool’s arguments equal its REST path’s query parameters; every parameter and property has a description; the signal vocabulary has one source.

Fixed

  • The gateway counted a shard’s 4xx/5xx with a JSON body as an answer with no rows and partial: false. A non-2xx is now a failed shard.
  • A bodyless 404 from a shard that does not serve a path (an older build during a rolling deploy) is reported as unreachable (503, retry), never as a cacheable absence.