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

GET /entities

GET https://api.unzoi.com/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

  1. Every value the index holds for type in the window is normalised to its key. Values that share a key are one group, with one entity_id.
  2. canonical is the group's most frequent spelling, the one to show. Every spelling is listed under aliases with its own count, and articles is the total across them.
  3. Your name is normalised the same way and compared with each group's key. match on 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 (IBM against international business machines). confidence follows as 1.0, 0.8 or 0.6. Groups are ranked by confidence, then by articles.

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_id as entity_id. A raw alias value, 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 asml and ASML are one filter. They are not fuzzy: organization=asml does not match asml holding nv. entity_id=organization:asml matches 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.
  • resolved says what each id matched. It lists {entity_id, aliases_used} per id. An id with no spelling in the window matches nothing, and its aliases_used is 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 as organization=asml costs 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" } }