GET /entities
Which spellings the index holds for a name.
Filters are exact matches on the index's own values, so resolve a name first: this groups every spelling that shares a normalised key, with a stable entity_id, a canonical spelling, per-alias counts and a confidence. Pass a returned `value` verbatim as the matching filter. Approximate by design -- ids are deterministic for a resolution version, not a registry. Equivalent to the `resolve_entity` MCP tool. Credits: 1-2; the second pass runs only when the name's own words match nothing. Pass estimate=true to get the exact range without running the call, or max_credits to refuse anything costlier.
Filters are exact matches on the index's own values. organization=ASML Holding NV matches nothing
while organization=asml matches thousands, and a zero-result response does not say which spelling the
index holds. This endpoint does. Give it a name as a user wrote it. It returns every spelling the index carries for
that name in the window, grouped under one entity_id, with a count per spelling and a confidence for the
match. Pass the id back as entity_id and it matches every spelling at
once.
Parameters
| Name | Type | Description |
|---|---|---|
| q required | string | The name to resolve, as the user gave it (any spelling or case). Matched against the index's own values after normalisation. |
| type required | `organization` · `person` · `location` · `city` · `region` · `name` · `topic` · `author` · `source` | Which entity field to resolve against; the returned `value`s are filters on it. |
| limit | integer | Results per page, 1-100 (default 10). |
| from | string | Lower time bound, e.g. 2026-07-09 or 20260709120000. Clamped forward to what the caller's plan may reach; see `history_days` and `from_clamped` in the response. |
| to | string | Upper time bound, same formats as `from`. |
| estimate | boolean | Return this call's price instead of running it: the credit range, a breakdown, your remaining credits and whether the call would run. A preview costs 0 credits. |
| max_credits | integer | Refuse the call, at no cost, when its worst case would cost more than this many credits. The refusal carries the estimate. |
type is the entity field to resolve against, one of
organization, person, location, city, region, name, topic, author, source. Pass a window: the counts are over it, the ids
are resolved within it, and without one the lookup walks the whole archive.
Response
| Field | Type | Description |
|---|---|---|
| approximate | boolean | Whether the counts came from a capped facet rather than the exact match set. |
| attribution | string | Required source attribution for any use of these results. |
| coverage | `complete` · `timed_out` · `range_too_wide` · `recent_unavailable` · `field_unavailable` | Why the answer is or is not complete, named for what you can do about it. `complete`: everything in range was read (or provably could not have changed the page). `timed_out`: the query ran out of budget -- retry, or narrow the range. `range_too_wide`: the range spans more of the corpus than one request may read -- narrow it; retrying unchanged will not help. `recent_unavailable`: the most recent data in range could not be read -- retry shortly; widening the range will not help. `field_unavailable`: a filter needs a field part of the index was built without (geographic, event-class and amount fields arrive with each shard's rebuild) -- the answer covers only the rebuilt part; retrying before the rebuild will not help. |
| credits_charged | integer | Index queries this answer took: one, or two when the window had to be faceted. |
| entities | EntityGroup[] | Matching groups, best match first. Empty means the index holds no such spelling in the window -- try a wider window or the plain `q` search. |
| from_clamped | boolean | Whether the window was moved forward to the plan's boundary. |
| history_days | integer | How far back the caller's plan may query; null = the full archive. |
| limit | integer | The cap that was applied. |
| partial | boolean | Whether this answer is incomplete. `true` means part of the index could not be read, so an empty or short result set is NOT evidence of absence -- retry rather than caching it as a negative. |
| q | string | The name as given. |
| type | `organization` · `person` · `location` · `city` · `region` · `name` · `topic` · `author` · `source` | The entity field that was resolved against. |
EntityGroup
| Field | Type | Description |
|---|---|---|
| aliases | EntityAlias[] | Every spelling in the group, most frequent first. |
| articles | integer | Articles across every alias in the window. |
| canonical | string | The most frequent spelling: the one to show and to filter on. |
| confidence | number | How the group matched the name: 1.0 exact, 0.8 prefix on a word boundary, 0.6 acronym. |
| entity_id | string | `<type>:<normalised key>`. Stable for a resolution version; not a registry id. |
| entity_resolution_version | string | The normalisation the ids were computed with. |
| type | `organization` · `person` · `location` · `city` · `region` · `name` · `topic` · `author` · `source` | The entity field the group was resolved on. |
EntityAlias
| Field | Type | Description |
|---|---|---|
| count | integer | Articles carrying this value in the resolved window. |
| match | `exact` · `prefix` · `acronym` | How this value matched the name given. |
| value | string | The value as indexed; pass it verbatim to the matching filter. |
Entity ids
An id is <type>:<key>: organization:asml. The type is the field the group was
resolved on. The key is the spelling, normalised: ASCII-folded, lower-cased, punctuation stripped, whitespace
collapsed and a leading "the" dropped. Organizations also lose trailing corporate suffixes (inc, ltd, nv, gmbh,
holdings and the like), and people lose honorifics. A key keeps its spaces, as in
person:christophe fouquet, so URL-encode ids.
entity_resolution_version, currently er-v1, names that normalisation. Within a version an
id is stable: the same raw value yields the same key on every request, and an id you stored still works. Across
versions it is not. An id is also not an identifier in any external registry; there is no Wikidata or LEI behind it.
Store the version next to any id you keep.
Alias groups
-
Every value the index holds for
typein the window is normalised to its key. Values that share a key are one group, with oneentity_id. -
canonicalis the group's most frequent spelling, the one to show. Every spelling is listed underaliaseswith its owncount, andarticlesis the total across them. -
Your name is normalised the same way and compared with each group's key.
matchon each alias says how it matched:exact,prefix,acronym. That is: the keys are equal; one is a prefix of the other on a word boundary; or the name is the initials of a multi-word value (IBMagainstinternational business machines).confidencefollows as 1.0, 0.8 or 0.6. Groups are ranked by confidence, then byarticles.
The lookup runs up to two facets over the field. The first is narrowed to articles whose text matches the name. The
second, run only when the first groups nothing, covers the whole window, which catches a spelling the extractor
produced from text that never wrote the name your way. Either way the counts are over the window.
approximate: true says they came from a capped facet rather than the exact match set.
Why two spellings can still be two ids
Normalisation works on strings, and it knows nothing about the entity behind them. Spellings of one entity that
normalise to different keys stay two groups with two ids: a transliteration, a nickname, a former name, a full name
and its acronym. Resolve IBM and you can get organization:ibm at confidence 1.0 and
organization:international business machines as an acronym match at 0.6. They are one company and two
ids. That is a limit of the normalisation, not a bug or a defect in the data, and the lower confidence says the second
match was made on string evidence alone.
Several ids in one entity_id must all match, so passing both asks for articles that carry both
spellings. To cover either, run the query once per id and merge on story_id or article
id. Or ask the user which one they meant.
Reading the result
- One group, confidence 1.0. Pass its
entity_idasentity_id. A raw aliasvalue, passed verbatim to the matching filter, still works for exactly that one spelling. - Several groups. The name is ambiguous, or one entity is split across two keys. Pick by
articles, look at the aliases, or ask the user. - An empty list. The index holds no such spelling in the window. That is not the same as the entity
being absent from the news: the extractor may not have produced it, or the coverage may be outside the window.
Widen the window, or search the words with
q. - Case does not matter; spelling does. Filters are case-insensitive, so
asmlandASMLare one filter. They are not fuzzy:organization=asmldoes not matchasml holding nv.entity_id=organization:asmlmatches both.
Filtering on an id
entity_id works on /aggregate, /search, /stories and /top-headlines,
on their tools (search_news, list_stories, top_headlines, aggregate_news),
and in a watch's query.
- Up to five ids, comma separated. Over REST the parameter may also repeat:
entity_id=organization:asml&entity_id=person:christophe%20fouquet. - Each id matches any spelling in its group, within the request window. The group is resolved again on every request, over the window you asked for. A spelling that appears only outside the window is not used, and a spelling that is new this week is.
- Several ids must all match. Two ids ask for articles that name both entities.
-
resolvedsays what each id matched. It lists{entity_id, aliases_used}per id. An id with no spelling in the window matches nothing, and itsaliases_usedis empty. The response is then empty because the id matched nothing, not because nothing was written. - Resolving an id costs an index query. It is the lookup this endpoint does, 1 or 2 credits per
id, and it is included in the call's
credits_charged. A raw filter such asorganization=asmlcosts nothing extra.
| Field | Type | Description |
|---|---|---|
| aliases_used | string[] | The spellings it matched in the requested window. Empty means none: the id matched nothing, which is not the same as no coverage. |
| entity_id | string | The id as passed. |
An id that is not <type>:<name> with a published type, or more than five ids, is refused
with 400 and code: invalid_parameter before any index work. The same ids are where
relationship queries start.
Example
curl -s -G "https://api.unzoi.com/entities" -H "x-api-key: $UNZOI_KEY" \
--data-urlencode "type=organization" \
--data-urlencode "q=ASML Holding NV" \
--data-urlencode "from=2026-08-01" {
"type": "organization",
"q": "ASML Holding NV",
"entities": [
{
"entity_id": "organization:asml",
"type": "organization",
"canonical": "asml",
"aliases": [
{ "value": "asml", "count": 1240, "match": "exact" },
{ "value": "asml holding nv", "count": 31, "match": "exact" }
],
"articles": 1271,
"confidence": 1.0,
"entity_resolution_version": "er-v1"
}
],
"approximate": false,
"limit": 10,
"credits_charged": 1,
"partial": false,
"coverage": "complete",
"history_days": 365,
"from_clamped": false,
"attribution": "Data derived from the GDELT Project (https://www.gdeltproject.org/)."
} Then filter on the id:
curl -s -G "https://api.unzoi.com/stories" -H "x-api-key: $UNZOI_KEY" \
--data-urlencode "entity_id=organization:asml" \
--data-urlencode "from=2026-08-01" \
| jq '{resolved, credits_charged, stories: [.stories[:2][] | {title, outlets}]}' {
"resolved": [
{ "entity_id": "organization:asml", "aliases_used": ["asml", "asml holding nv"] }
],
"credits_charged": 2,
"stories": [
{ "title": "Chip equipment orders climb as foundries add capacity", "outlets": 41 },
{ "title": "Dutch export licence rules tightened for lithography tools", "outlets": 27 }
]
}
Two units: one for the stories page and one to resolve the id. The raw filter,
organization=asml, would have cost one and missed the articles spelled asml holding nv.
Errors
| Status | Meaning |
|---|---|
| 200 | Success |
| 400 | Invalid request, charged nothing. Bounds, enums, dates and offsets are validated BEFORE any index work, so this never means a partially-served query. `credit_budget_exceeded`: the call's worst case is more than `max_credits`. |
| 401 | Missing or invalid API key. `code` is `unauthorized`. |
| 402 | Account suspended (subscription canceled or payment failed). `code` is `suspended`. |
| 429 | Refused before running, and charged nothing. `credit_rate_limited`: this minute's credits are spent. `message_rate_limited`: too many free calls or refused requests this minute. `insufficient_credits`: the key stops at its monthly allocation and does not have this call's worst case left. `retry-after` says how long to wait. |
| 503 | The index could not be read for this request. This is NOT an empty result set and NOT a statement that nothing matched — retry rather than caching it as a negative. `coverage` says which part was unreadable. |
A type that is not one of the entity fields is 400 with code: invalid_parameter.
Over MCP
The same request is the resolve_entity tool. It is step zero of
the agent workflow: resolve the name, then pass the entity_id it returns to the next call.
{ "name": "resolve_entity", "arguments": { "q": "ASML Holding NV", "type": "organization", "from": "2026-08-01" } }
{ "name": "list_stories", "arguments": { "entity_id": "organization:asml", "from": "2026-08-01" } }