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_unitsiscredits_charged, andx-request-unitsisx-credits-charged.x-quota-limit/-used/-remaining/-resetarex-credits-limit/-used/-remaining/-reset.x-ratelimit-limit/-remaining/-resetarex-credits-per-minute/-per-minute-remaining/-per-minute-reset, and count credits.- Error code
quota_exceededisinsufficient_credits, andrate_limitediscredit_rate_limited, ormessage_rate_limitedfor free calls: MCP messages, previews,/account, and requests refused before they ran. Refusal bodies carry the call’screditsrange and the figures that refused it. /accountandaccount_statusreportcredits_limit,credits_used,credits_remaining,credits_per_minute,credits_reset_secsandcredits_charged(0);account_statusaddscredits_this_session. The control API’s tenant addscalls_this_period.- Plans report
monthly_credits,credits_per_minuteandoverage_cents_per_1k_credits;/v1/plansaddscredit_costs. - The control API’s usage reports
creditsandcallsper day and per operation. - The Stripe billing meter is
credits. - Watch status
paused_quotaispaused_credits. /storiesrefuses anoffsetabove 1,000.
Added
estimate=trueon every endpoint and tool returns the call’s credit range, a breakdown and whether it would run, without running it, for 0 credits.max_creditson 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.jsonends with its price;/docs/credits.mdhas 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_watchwithestimate) addsper_run,runs_per_monthandwebhook_attempts_per_event_max. - Query analytics events record
credits, and the rollups carry acreditscolumn (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_related2. Offsets cost 1 per full 1,000 rows on/searchand/top-headlinesand 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, MCPinitializeandtools/listare 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_newsover 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,longitudewithradius_km(0.1-2000), orbbox=south,west,north,east, on/search,/stories,/top-headlinesand 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 carrymatched_locations(name, country, coordinates and, fornear,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_typeon the same endpoints and/aggregate, a facet of the same name, andevent_typeson every article: seventeen classes (events-v1) derived from GKG themes, among thembankruptcy,earnings_report,ipo,cyber_attack,outage,executive_change,labor_strikeandsupply_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 anexecutive_changeonly 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_minandamount_max: articles mentioning a numeric amount within the bounds. One amount must satisfy every bound, and withamount_objectthat amount must describe the object.amounts[].numberon/doc/{id}is the value compared.coverage: field_unavailable: a filter needs a field part of the index was built without. The answer ispartialand 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_unitscounts the batches. When the candidates outnumber the scan,totalis estimated andtotal_relationisapproximate, and facets, which count candidates, are marked approximate.
Fixed
x-request-unitsis documented among the response headers in/openapi.json; it was sent but not listed.- A graph edge’s
shareis 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-hopconnection_pathcounts the starting entity, so its direct edge’sshareis no longer 0. - A graph edge’s
typelists the seven entity fields neighbours come from, not all nine. account_statuscosts 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_moreis false once those run out instead of promising a page it cannot serve.cursorcannot be combined with these filters. - The gateway keeps a shard’s
approximatetotal, so a scan that ran out of candidates is not reported asexact. - 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_minandamount_maxrefuse 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_idon/search,/stories,/top-headlines,/aggregateand 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’sresolvedlists the spellings each id used; an id with none in the window matches nothing and says so with an emptyaliases_used.GET /graph/relatedandrelated_entities: the organizations, people, places and topics co-mentioned with one entity, strongest first, each withco_mentions,share, and for the strongest (evidence, default 3) the newest shared articles with distinctsources,stories,first_seenandlast_seen.GET /graph/pathandconnection_path: the direct co-mentions of two entities and up to five paths through one shared neighbour (max_hops1 or 2).GET /graph/networkandentity_network: neighbours grouped by type, and withcompare_from/compare_tothe neighbours that are new or gone and the ties whose share grew by half or fell by a third.GET /graph/exposuresandshared_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,partialandcoverage. The corpus can be scoped bylanguage,publisher_country,source_typeandcountry. Bounds: two hops, 50 neighbours (20 per type on a network), 10 evidence articles and 16 index queries per index. request_unitsin search, story, entity, aggregate and graph responses, and thex-request-unitsheader on every authenticated REST response. A call is charged the index queries it issued: a search costs 1; resolving anentity_id, asignal_percentile_minthreshold, 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 forGET /watches/{id}.- The webhook body is published as the
WatchWebhookschema underwebhooksin/openapi.json; watch events carryrecorded_at.
Changed
- A watch’s label is
label. In 0.4.0 it wasname, 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.namein 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_outletsandcurrent_outletsonnew_storyrather than sendingnull. - Watch routes document
500(the watch store failed) instead of503, which they cannot return.
Fixed
paused_quotais 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 thecreate_watch,list_watches,delete_watchandwatch_eventstools. A watch is a/storiesquery (the filters,q,modeand signals it may use),interval_minutes,minimum_outlets(default 2),notify_on(new_story,story_growth) and an optionalwebhook_url.new_storyfires when a story first reachesminimum_outlets;story_growthwhen it crosses 5, 15, 40 or 100 outlets, or grows by 10 since its last event. Events are kept seven days and page withafter.- 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-deliveryandx-unzoi-signature: sha256=<HMAC-SHA256 of the body>under thewebhook_secretreturned 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
partialrecords 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.jsoncarrieswatchesandmin_watch_interval_minutes). Over the cap is400withcode: watch_limit. - Error codes
watch_limitandunsupported(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 /aggregateand theaggregate_newstool: per calendar bucket (hour,day,weekfrom Monday,month, orall), the article count and optionally distinctstories, distinctoutletsand a signal’s mean and percentiles (metrics=stories,outlets,signal:finance,percentile=90).intervalis required;fromcomes from the plan window when omitted; at most 366 buckets. Distinct counts summed across indexes are markedapproximate.sort=count|outlets|recencyon/storiesandlist_stories; each story carriespublished_atof its representative article.GET /stories/{id}reportsfirst_reported(the earliest member in the index),title_variants(distinct headlines among the listed articles) andsources_by_first_seen. These are proxies for origin; nothing in the index marks wire copy.cursoron/search,/top-headlinesand their tools: keyset paging over a newest-first browse. Passstart, then eachnext_cursor. Pages are deterministic and disjoint even when many articles share a timestamp. Not withoffset,qor a rankedmode.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 assignal_threshold; across indexes it is a count-weighted mean and markedapproximate.
Changed
aggregate_newsandresolve_entityrequire 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=asmlis now400 {"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-32602with the same hint; every tool now declaresadditionalProperties: false. - Unknown signal names are refused (
code: unknown_signal) instead of matching nothing;signal_min/signal_maxwithoutsignal, an unknownfacetsfield, and a malformedsignalsclause areinvalid_parameter. - 401, 402 and 429 bodies are JSON, the documented
Errorshape, instead of plain text. A missing document (404) now carries a body too. Errorgainscode(stable;invalid_parameter,unknown_parameter,unknown_signal,not_found,unauthorized,rate_limited,suspended,quota_exceeded,upstream_unavailable,internal) and requireserror,code,detail. MCP tool errors carry the same JSON in their text block; protocol errors carrycodeinerror.data.search_newsno longer requiresq: omit it for a newest-first browse, asGET /searchalways allowed.list_storiesandtop_headlinesgainsource,modeand the enums they lacked;top_headlinesno longer injectsmode=recent(it is implied)./storieshonoursmode. It was always keyword-ranked; the default is stillkeyword.
Added
GET /stories/{id}and theget_storytool: one story expanded — counts, outlets,first_seen/last_seen, facets over its members, the representative article, and its newest articles as compact hits.GET /entitiesand theresolve_entitytool: the spellings the index holds for a name, grouped by a deterministicentity_id(<type>:<normalised key>,er-v1) with aliases, counts and a confidence. Approximate by design; not a registry.story_idonsearch_news,list_storiesandtop_headlines, and in thefacetsenum.view=compacton/search,/top-headlines,/similar/{id}and the matching tools: each hit keepsid, title, url, source, published_at, language, story_id, score. Default staysfull.partialandcoverageon every search-shaped response, on both tiers and on/similar/{id}; the server instructions and tool descriptions state the rule: never conclude absence whenpartialis true,coverageis notcomplete, orfrom_clampedis true.outputSchemaon every MCP tool (the OpenAPI component, inlined), so a schema-aware host can validatestructuredContent.mode=recentdocumented;scoreand the previously undocumentedArticlefields documented;ArticleDetail’squotes,amounts,date_mentionsandlocation_detailstyped; thesignalparameter 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.