Resolution comes before enrichment
Entity Resolution compares the identity signals you already have against canonical entities. It returns a decision, not just a best guess: a strong match is resolved, an ambiguous match becomes review_required, and a record without sufficient evidence stays unmatched. Data Enrichment uses this same decision internally, so synchronous resolution and asynchronous enrichment do not disagree.
Resolve one organization or person
Use a stable record.id that can be correlated with your source system. Send only known identity fields and select strict, balanced, or broad confidence policy. The call requires the entities:resolve scope.
curl -X POST https://api.enrich.nordicdevhouse.com/v1/entity-resolution/resolve \
-H "Authorization: Bearer enrich_live_••••••••" \
-H "Idempotency-Key: subscription-nordic-accounts-v1" \
-H "Content-Type: application/json" \
-d '{
"record": {
"id": "crm_1842",
"type": "Organization",
"fields": {"company_name": "Northstar Logistics Oy", "business_id": "2841842-1", "country": "FI"}
},
"confidence_policy": "balanced"
}'Inspect the decision and its evidence
canonical_entity is populated only for resolved results. candidates remain ranked so review tooling can show credible alternatives. decision records the applied policy, threshold, margin, and reason; evidence explains which signals affected the score.
{
"client_record_id": "crm_1842",
"status": "resolved",
"entity_type": "Organization",
"canonical_entity": {"id": "org_fi_2841842_1", "type": "Organization", "label": "Northstar Logistics Oy", "properties": {"businessId": "2841842-1"}, "confidence": 0.98, "evidence_ids": ["ev_ytj_northstar"]},
"candidates": [{"rank": 1, "score": 0.999, "reasons": ["exact_business_id", "name_match", "country_match"], "entity": {"id": "org_fi_2841842_1", "type": "Organization", "label": "Northstar Logistics Oy", "properties": {"businessId": "2841842-1"}, "confidence": 0.98, "evidence_ids": ["ev_ytj_northstar"]}}],
"decision": {"policy": "balanced", "threshold": 0.9, "required_margin": 0.08, "top_score": 0.999, "margin": 0.129, "outcome": "resolved", "reason": "strong_identifier_match"},
"evidence": [{"id": "ev_ytj_northstar", "source": "prh_ytj", "observed_at": "2026-08-12T09:00:00Z", "url": "https://example.test/registry/northstar"}],
"review_case_id": null,
"resolved_at": "2026-08-15T12:00:00Z"
}Treat outcomes as separate states
| Status | Meaning | Application action |
|---|---|---|
| resolved | One candidate passes the threshold and margin | Use canonical_entity and retain the decision evidence |
| review_required | Credible candidates are too close or below automatic acceptance | Queue review_case_id and present the ranked candidates |
| unmatched | No candidate has sufficient identity evidence | Keep the source record unresolved or collect stronger signals |
Choose the acceptance policy by risk
| Policy | Trade-off | Recommended use |
|---|---|---|
| strict | Fewer automatic matches, lowest false-positive risk | Compliance, payments, master-data writes |
| balanced | Production default for mixed business records | CRM, analytics, routine enrichment |
| broad | More candidates and automatic matches | Discovery workflows with downstream review |
Ambiguous input creates a review case
A name such as Northstar without a business ID, domain, email, or other discriminating signal can match more than one organization. The API deliberately returns review_required with ranked candidates instead of silently selecting one.
Use the same decision in enrichment jobs
For one immediate identity decision, call /entity-resolution/resolve. For a durable batch that also needs accepted facts, submit /enrichment-jobs with the general-entity@1 profile. Each enrichment record embeds the same resolution result and evidence, and ambiguous records remain reviewable.
Calibrate policies with labeled records
POST /entity-resolution/evaluations runs up to 1,000 labeled records through strict, balanced, and broad policies without changing canonical data. Compare precision, recall, false positives, false negatives, and review rate before promoting a policy.
{
"policies": ["strict", "balanced"],
"cases": [{
"record": {"id": "known-1", "entity_type": "Organization", "fields": {"business_id": "2841842-1"}},
"expected_entity_id": "org_fi_2841842_1"
}]
}Review decisions update the durable result
List pending cases from /review-cases and send accepted, rejected, or unresolved with expected_version. Accepted selects the highest-ranked candidate, regenerates accepted facts, clears review_required, and emits a completion event. A stale expected_version returns 409 so two reviewers cannot overwrite each other.
Govern duplicates as workspace identity changes
POST /entity-resolution/duplicates/detect automatically finds likely duplicates and explains the contribution of identifiers, normalized names, country, domain, email, and phone. Detection never merges silently: identity:write callers accept or reject a proposal against an expected graph release, resolve conflicting properties explicitly, and publish a new immutable release. An accepted cluster keeps append-only history and can be split back to its exact pre-merge snapshot while that merge release remains active.
Production checklist
| Control | Requirement |
|---|---|
| Stable IDs | Reuse the source-system record ID across retries |
| Strong signals | Prefer business IDs, domains, and verified emails over name alone |
| State handling | Model resolved, review_required, and unmatched explicitly |
| Audit trail | Persist policy, candidate ranking, evidence, and observed time |
| Evaluation | Measure precision and review rate on labeled records before promotion |