/
resolve()
Send text. Resolve it against Wikidata, your WordLift knowledge graph or your own entities. Get back a stable identifier for each mention, or unresolved when the evidence isn't sufficient. English, Italian, French, German, Spanish and Portuguese; Japanese and Chinese answer too.
Resolver
POST /accounts/{id}/resolve
curl -X POST "https://api.wordlift.io/accounts/$WL_ACCOUNT_ID/resolve" \
-H "Authorization: Key $WL_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "{{heroText}}",
"language": "en"}'
import os, requests
r = requests.post(
f"https://api.wordlift.io/accounts/{os.environ['WL_ACCOUNT_ID']}/resolve",
headers={"Authorization": f"Key {os.environ['WL_KEY']}"},
json={"text": "{{heroText}}", "language": "en"},
)
print(r.json())
import { WordLiftResolver } from "@wordlift/resolve"; // npm i @wordlift/resolve const wl = new WordLiftResolver({ apiKey: process.env.WL_KEY! }); const res = await wl.resolve({ text: "{{heroText}}", language: "en", });
→ 200 OK // 542 ms, live engine {"mentions": [ {"text": "Apple", "start": 0, "end": 5, "status": "resolved", "entity": {"id": "Q312", "label": "Apple Inc.", "types": ["Organization"], "same_as": [...]}, "score": 0.9977, "signals": {"path": "reranked", ...}}, {"text": "Rome", "start": 28, "end": 32, "status": "resolved", "entity": {"id": "Q220", "label": "Rome", "types": ["City"], ...}, "score": 0.9982, ...}], "dataset_uri": "wikidata://public", "language": "en"}
→ 200 OK // live engine {"mentions": [ {"text": "Apple Street Bakery", "start": 0, "end": 19, "status": "unresolved", "entity": null, "reason": "low_relevance", "signals": {"path": "reranked", "match_probability": 0.0, ...}}, {"text": "Rome", "start": 44, "end": 48, "status": "resolved", "entity": {"id": "Q220", "label": "Rome", "types": ["City"], ...}, "score": 0.9951, ...}], "dataset_uri": "wikidata://public", "language": "en"}
The pipeline, the Resolver interface and the clients are Apache-2.0: extraction, your retrieval, your graph. WordLift runs the decision engine behind POST /accounts/{id}/resolve. You keep control of your content and your candidate universe.
Wikidata is the default, not the limit. Point resolve() at your own entities and every answer is one of your ids, or unresolved.
Nothing to upload or configure. Send dataset_uri: "wordlift://dataset/me" and the engine reads your graph with the same key: names, aliases, descriptions, types and sameAs links. Answers carry your IRIs.
A product catalogue, a taxonomy, a CRM export. Send up to 5,000 entities as dataset.entities with id, name, aliases, description, types and same_as. Any ids you like come back unchanged.
"wordlift://dataset/me,wikidata://public" tries your graph, then Wikidata, per mention. Each answer says which world it came from, and a Wikidata match your graph declares sameAs comes back as your entity with the public ids merged in.
wikidata://public ─┐ default · 13.6M entities wordlift://dataset/me ─┤ your KG, via your key dataset.entities ─┤ inline · up to 5,000 mentions[].candidates ─┘ your shortlist · public │ ▼ resolve() ──► your id | unresolved
{"text": "Our new Rome office opens in May.",
"dataset_uri": "wordlift://dataset/me,wikidata://public"}
# per mention: your graph answers first;
# otherwise Wikidata; otherwise unresolved.
# each mention reports the world that answered
# in its own "dataset_uri".
{"text": "ACME Corp opened an office in Rome.",
"language": "en",
"dataset": {"entities": [
{"id": "https://acme.example/kg/acme",
"name": "Acme", "aliases": ["ACME Corp"],
"types": ["Organization"],
"same_as": ["https://www.wikidata.org/entity/Q1319384"]},
{"id": "https://acme.example/kg/rome-office",
"name": "Rome", "description": "Acme's office in Rome",
"types": ["Place"]}]}}
{"mentions": [
{"text": "ACME Corp", "start": 0, "end": 9,
"status": "resolved",
"dataset_uri": "inline",
"entity": {"id": "https://acme.example/kg/acme",
"label": "Acme", "types": ["Organization"],
"same_as": ["https://www.wikidata.org/entity/Q1319384"]}},
{"text": "Rome", "start": 30, "end": 34,
"status": "resolved",
"dataset_uri": "inline",
"entity": {"id": "https://acme.example/kg/rome-office",
"label": "Rome", "types": ["Place"]}}],
"dataset_uri": "inline", "language": "en"}
Every match still goes through the same context ranking and abstention gate as Wikidata: a name that is not in your data is no_candidates, a name whose context does not fit is low_relevance. The walkthrough with curl, Python, TypeScript and the command line: Run resolve() on your own data.
In production a confident wrong link costs more than no link. It enters the graph, ships in JSON-LD and gets repeated by every system downstream. resolve() is built for precision, calibration and explicit NIL.
The graph gains an edge you can trust.
A false fact in your graph, markup and agent answers. Hard to find, costly to unwind.
The mention stays unlinked. It can be reviewed, or linked later when the right entity exists.
Measured against Google Cloud Natural Language on 2026-10-02, on gold sets neither side chose. resolve() answers more mentions and Google is more often right about the ones it answers: we lead in English, German and Spanish and trail in the other five. A stricter decision layer with an explicit NIL is evaluated separately, below, and is not in production. We publish the misses too.
| set | mentions | resolve() recall / precision / F1 | Google Cloud NL recall / precision / F1 | F1 difference |
|---|---|---|---|---|
| English · gold set | 100 | 89.0 / 91.8 / 90.4 | 82.0 / 96.5 / 88.6 | +1.8 |
| English · hard names | 266 | 70.3 / 82.4 / 75.9 | 55.3 / 88.6 / 68.1 | +7.8 |
| German | 86 | 93.0 / 96.4 / 94.7 | 87.2 / 98.7 / 92.6 | +2.1 |
| Spanish | 83 | 86.7 / 93.5 / 90.0 | 81.9 / 94.4 / 87.7 | +2.3 |
| Italian | 83 | 86.7 / 88.9 / 87.8 | 85.5 / 95.9 / 90.4 | −2.6 |
| French | 73 | 82.2 / 87.0 / 84.5 | 79.5 / 95.1 / 86.6 | −2.1 |
| Portuguese | 84 | 90.5 / 96.2 / 93.3 | 96.4 / 98.8 / 97.6 | −4.3 |
| Japanese | 79 | 70.9 / 84.8 / 77.2 | 74.7 / 89.4 / 81.4 | −4.2 |
| Chinese | 78 | 59.0 / 82.1 / 68.7 | 70.5 / 79.7 / 74.8 | −6.1 |
| AIDA-B · 230 news articles | 3138 | 73.9 / 87.8 / 80.3 | 64.8 / 82.2 / 72.5 | +7.8 |
| MSNBC · 20 long articles | 317 | 68.8 / 86.2 / 76.5 | 78.2 / 89.9 / 83.6 | −7.1 |
| Eight gold sets | 666 | 82.7 / 90.6 / 86.5 | 82.4 / 93.8 / 87.8 | −1.3 |
Same gold sets and the same matching rule for both systems. Recall is correct over all gold mentions, precision is correct among the links made. Google is the v1 analyzeEntities API, its Wikipedia links mapped to Wikidata ids; it publishes no confidence, so this is its only operating point. Our side is the production pipeline replayed on the same sets. The gold sets are built from Wikipedia and hold 73 to 100 mentions per language, so a difference under about five points is within noise. The two long-document sets are public news corpora (AIDA-B, MSNBC) run end to end on 2026-10-09 with both systems detecting their own mentions; on AIDA-B we lead by 7.8 points on 3,138 mentions, on MSNBC Google leads by 7.1 on 317. Both are on the page because the rule is to publish the misses with the wins.
The decision layer: a second step that must be able to say NONE. Single frozen runs on held-out sets, 2026-09-23; German replayed on all three of its sets on 2026-09-27. The criteria were fixed before each run: accepted precision ≥ 99%, resolution coverage ≥ 90%, false resolution under forced NIL ≤ 2%. No language has cleared all three yet, so this layer stays on hold and these sets are now regression sets.
| language | accepted precision ≥ 99% | resolution coverage ≥ 90% | false resolution · forced NIL ≤ 2% | false resolution · natural NIL reported | verdict |
|---|---|---|---|---|---|
| DE · frozen rule, three holdouts replayed | 129/130 · 99.2% | 129/145 · 89.0% | 3/145 · 2.07% | 0/3 | hold |
| IT | 37/37 · 100% | 37/37 · 100% | 3/37 · 8.1% | 1/2 | hold |
| EN | 41/41 · 100% | 41/57 · 72% | 1/57 · 1.8% | 0/6 | hold |
German row: the rule frozen before the third gate, replayed once on all three held-out sets (145 mentions, 2026-09-27). The three gates themselves, one per set on 2026-09-23, were 100% / 90% / 5.0%, 98% / 88% / 3.5% and 100% / 94% / 2.1%; the rule changed on development data between them, never on a holdout. On 145 mentions the verdict is hold by one false resolution and two links.
If the correct entity never reaches resolve(), that is a retrieval problem. If it is present and the wrong identity is selected, that is a resolution problem. We report them separately so every error has an owner.
gold ∈ candidates ? ├── no → retrieval miss # candidate recall └── yes → resolve() returns gold ? ├── yes → correct resolution └── no → resolution error # accuracy
resolve() sits after extraction and candidate retrieval. It reasons over the mention, context, candidates and retrieval evidence, then commits or abstains.
ner("Il sindaco di Roma ha presentato il piano.") → Roma : LOCATION // a type label
resolve("Roma", ctx, candidates) → wd:Q220 // Rome, a stable identifier
Your graph defines the world. Retrieval introduces your entities. resolve() decides whether a mention actually refers to them.
Retrieval systems, vocabularies and knowledge graphs change without retraining a specialized classifier every time the entity universe grows. Point resolve() at your own world: your WordLift knowledge graph, derived from your API key, or a vocabulary sent with the request. Local entity first, Wikidata for the rest, with sameAs links merging the two.
The decision layer is operated by WordLift. No external classification service in the resolution path, and the models behind it can change without changing the contract.
resolve() answers in six languages today; Japanese and Chinese answer too, unmeasured so far. The NIL-safe decision layer is released one language at a time, each against immutable held-out fixtures and a gate whose criteria are fixed before the run.
Entity linkers have mostly been built to always answer: take the top-ranked candidate and move on. resolve() is built to answer when the evidence supports it.
Resolve against your WordLift graph, read with your key: your IRIs come back, public ids merged through sameAs.
Product catalogues, taxonomies, CRM entities. Your ids in, your ids out.
Explicit unresolved instead of guesses, signals on every answer, per-language gates on frozen held-out sets.
Dedicated capacity, volume and latency agreements, onboarding on your own graph.
The Content Analysis pipeline is open source. Use it to extract mentions, connect your own candidate retrieval and knowledge graph, inspect the data flow, and build on it.
resolve() adds WordLift's proprietary entity-resolution engine at the decision point: send text, or mentions with your own candidates, and get the supported identity for each, or unresolved.
extract(document)
│
▼
candidate retrieval ──────►
│
│
your KG / vocabulary
WordLift enginewe run it► resolve( mention, context, candidates ) │ ├── entity └── NIL
Inspect the code, run the pipeline locally, bring your own entities and candidate retrieval, and plug in any resolver. Apache-2.0.
View on GitHub →Point the pipeline's WordLiftResolver at POST /accounts/{id}/resolve for multilingual entity disambiguation, explicit NIL and managed inference. Same shape, no code change.
Get a free key →$ git clone https://github.com/wordlift/content-analysis.git $ cd content-analysis $ pip install -e ".[ner]"
from resolve_pipeline import run, WordLiftResolver results = run( document, retrieve=my_retriever, # candidates from your KG resolver=WordLiftResolver(api_key=WL_KEY), ) # → per mention: resolved entity, or unresolved
$ curl -X POST "https://api.wordlift.io/accounts/$WL_ACCOUNT_ID/resolve" \ -H "Authorization: Key $WL_KEY" -H "Content-Type: application/json" \ -d '{"text": "Apple opened a new store in Rome.", "language": "en", "mentions": [{"text": "Apple", "start": 0, "end": 5, "candidates": [{"id": "wd:Q312"}, {"id": "wd:Q89"}]}]}' {"mentions": [{"text": "Apple", "start": 0, "end": 5, "status": "resolved", "entity": {"id": "wd:Q312", "label": "Apple Inc.", "types": ["Organization"], "same_as": ["https://www.wikidata.org/entity/Q312"]}, "score": 0.99, "signals": {"path": "reranked", "retrieval_prior": 0.87, "match_probability": 0.99, "rescued": false}}], "dataset_uri": "wikidata://public", "language": "en"} # status is "resolved" or "unresolved"; unresolved is an answer, never guess. # score is relevance in [0, 1]; read signals.path before comparing scores across mentions. # warm requests answer in about a second (engine p50 779 ms, measured 2026-09-27); a graph's first call pays its fetch. # omit "mentions" to let the engine detect them; omit "candidates" to retrieve against the dataset. # npm install @wordlift/resolve · pip install "resolve-pipeline @ git+https://github.com/wordlift/content-analysis.git"
Four short questions. We read every answer and reply with a key, a plan or a straight "not yet" for your case, usually within a working day.
Free for individual developers: 1,000 requests a month. Long texts count more: one request, plus one per 1,000 characters, so about 160 articles. Four short steps; the key arrives by email.
Meanwhile, the open pipeline and the clients are on GitHub, and the live tester above runs against the same engine.
{{leadHint}}
We'll use these details to follow up about resolve(). Read our Privacy Policy.