Skip to main content

Identity resolution and merge semantics

Every segment membership, funnel, attribution model, and routing decision is only as correct as the contact row it reads. This page defines how an incoming identifier becomes one contact — which keys resolve, which merge direction wins, what each fold-in writes onto the survivor, and what updates downstream — so you can reason about the duplicate or split-contact shapes the operator surfaces show. The operational guide (queue, console, undo window) is Identity resolution; the shape of the upstream stream is The CDP event model.

Section 1 — Identity keys and precedence

Four keys anchor a contact. Each is normalized before it enters the identity index, so formatting never decides a match: Precedence is tenant-owned. PUT /api/v1/cdp/identity-rules authors one rule per identifier type (external_id, email, phone, anonymous_id) with a numeric priority; a lookup matches on the strongest enabled rule at the time it runs. An operator who imports from a CRM typically puts external_id first so the CRM key, not a respelled phone, settles the match. The rule set resolves lookups everywhere — ingest binding, imports, inbound messaging, and the stitch API. Anonymous traffic is a first-class key, not noise. Events carry one or both of userId and anonymousId; they hold contact_id = NULL until the identity pass binds them (see The CDP event model, Section 2). When an identify later pairs that anonymous_id with a strong phone or email, the pre-identify session folds onto the resolved contact in the same request.

Section 2 — Deterministic versus probabilistic resolution

Orbit runs two resolution layers on the same identifier sets, and the distinction decides what a duplicate scan can do unattended:
  • Deterministic. Rows whose normalized identifiers are equal on a declared key group transitively into one profile — the email plus phone pair this-and-that corroborate each other, so equality, not similarity, does the match. POST /api/v1/cdp/identity-resolution/preview dry-runs the groups without writing; POST /api/v1/cdp/identity-resolution/run auto-merges each group whose confidence clears your threshold and leaves the rest as a review queue.
  • Probabilistic. The remainder — a fuzzy name, a phone-format respelling, a shared non-unique channel id (WhatsApp or Viber) — ranks into a review band instead of merging. GET /api/v1/cdp/identity/merge-candidates returns ranked pairs with a confidence and a band; min_confidence and include_auto_merge=false bound the scan to the review queue. A phone-only link whose members carry different emails (the shared-handset or reassigned-number ambiguity) lands here by design — it is a steward’s call, never an automatic fold.
Set the auto-merge threshold deliberately: the run pass applies only the groups at or above it, and a dismissed pair re-enters the queue only if the underlying data changes. When you change a rule, the simulator (POST /api/v1/cdp/identity-resolution/simulate) previews which pairs the new rule would collapse and raises an over-merge warning — run it before enabling so a too-loose rule fails in preview, not on live contacts.

Section 3 — Merge direction and survivorship

The primary record survives; secondaries fold in and are marked merged-away so channels cannot re-contact them as distinct people. Field-level survivorship is decided by three sources, in a strict precedence:
  1. Per-request pins (fieldStrategies, plus attribute.<key> pins for custom fields) — set on the merge call itself, highest precedence.
  2. Your tenant survivorship policy (GET/PUT/DELETE /api/v1/cdp/survivorship-policy) — a rule per governed field over the scalar contact columns (phone, email, WhatsApp id, Viber id, name, company, country code, timezone, language, external id). Strategies are prefer_target / prefer_source, most_recently_updated / most_recently_created, prefer_non_null, and prefer_source_system (value from the record whose source is a named system — crm, salesforce, import).
  3. The safe default — blanks and nulls never overwrite a populated value; a partial update cannot erase an established trait.
Two properties make the fold auditable rather than mysterious:
  • Reversal is bounded, not vague. Every merge returns a merge_id; the undo window stays open for 30 minutes, and POST /api/v1/contacts/unmerge/:mergeId restores the split while it is open. GET /api/v1/contacts/merge-history is the full audit list.
  • Every merge writes a durable audit row recording the verdict, the confidence, and the identifiers that corroborated the match — the GDPR art. 15/22 “how did this decision happen” answer. A merge you applied through the review queue carries the steward’s verdict on the same row.
Provenance follows the survivor: the source-of-truth ordering you set in the policy decides which record’s value a given field reads, and the audit trail records which fold contributed it. A merged-to contact is a synthesis with a paper trail, not a guess.

Section 4 — Downstream effects

A merge redirects channel membership, tags, custom-field values, conversation history, and CDP events onto the survivor — so consumers that read the contact row re-derive from the unified profile on their own cadence:
  • Segments. The same fold-in that redirects events also redirects the contact’s segmentable fields; auto-refresh segments pick the change up on their next evaluation cadence, the scheduled refresh converges the rest. See How CDP segments recompute.
  • Predictive attribute routing. Inbound voice routing matches on the resolved contact’s attributes (score and trait tags). After a merge, a caller’s routing verdict derives from the survivor’s profile — a routing rule that once split one person across “high value in one record, no score in the other” now reads one profile consistently.
  • Conversation threading. Inbound messaging, voice, and video resolve by the bound phone or email; they read the survivor directly, so two channels that once opened parallel threads converge onto one timeline.
  • Computed traits and scores. Churn, CLV, and engagement scores recompute over the unified event history instead of splitting across two partial records.
Conversely, a split (a duplicate that has not merged) propagates the same way: membership counts it twice, routing reads two halves, and the conversation timeline shows parallel threads. That is the failure shape the operator surfaces below are for.

Section 5 — Where duplicates surface

Duplicates announce themselves on three read paths; check them when a count looks inflated or a caller history reads wrong:
  • The review queue — GET /api/v1/cdp/identity/merge-candidates or the dashboard Audience → Identity resolution page ranks open pairs; duplicates that never reach a merge sit here.
  • The contacts surface — GET /api/v1/contacts (or the merge-history feed) shows the survivor and its folded-in secondaries; a contact with an unexpected twin usually cleared the review queue or came back through a re-import.
  • Number-ownership conflicts — when the same phone claims two contact rows (common after a re-import that bypassed external_id), inbound resolution and screen-pop read an ambiguous owner until a merge settles it.
The fix is the same whichever surface you read it on: run the deterministic preview, take or dismiss the pair on the review queue, and let the 30-minute unmerge catch the rare over-merge.

Section 6 — Tenant-owned decider

Every behavior above is a tenant-controlled surface; the platform enforces no canonical merge policy:
  • Rule priority and identity keys are set on identity-rules.
  • The auto-merge threshold and the review band are set per tenant.
  • Survivorship precedence (pins → policy → blank-safe default) is set per tenant and per request.
  • Merges are reversible inside a 30-minute window and audited permanently, so an incorrect fold is recoverable, not silent.
Nothing here is a platform-mandated behavior claim: Orbit supplies the match contract and the audit trail; you decide what merges. The guide (Identity resolution) walks the surfaces above; this page defines what each of them guarantees.

Worked examples — script a dedupe cycle end-to-end

Section 2 names the operations; this section shows the exact request and response shapes so you can drive the whole cycle from a script or cron: preview the groups, run the auto-merge, read the review queue, undo a bad fold, and simulate a proposed rule change before it goes live.

1. Preview — group candidates without writing

Send a dry-run scan. The body is optional; auto_merge_threshold overrides the default for this call only.
Groups at or above the threshold land in auto_apply; the rest form review_queue. Reason values are masked because the default PII posture is mask-on-return:
Adjust max_contacts (default 5,000, ceiling 10,000) when truncated comes back true on a large workspace.

2. Run — auto-merge the high-confidence groups

Same scan, but this one applies the auto_apply groups (bounded by max_merges, default 50) and returns each group’s merge_id-equivalent identity (primary_id + merged_secondary_ids) plus whatever still needs a human.
If merge_budget_hit is true, re-invoke — each run processes up to max_merges secondaries and leaves the remainder in the queue for the next pass.

3. Simulate — test a proposed rule change before saving

Pass the PROPOSED (unsaved) rule set; the endpoint runs it over a bounded read of your live contacts and returns the blast radius. It never writes — the dashboard gates the identity-rules PUT behind a passing simulation.
Treat over_merge_detected: true as a stop: the named cluster would collapse more than over_merge_cluster_size contacts into one profile — the shared-handset failure shape. Re-run with a tightened rule set until it comes back false, and only then save the rules.

4. Review queue — rank the probabilistic candidates

The fuzzy remainder is a ranked list, highest confidence first. Bound it to the human band with include_auto_merge=false and a min_confidence (0–100) floor:
Approve a candidate with POST /api/v1/contacts/merge (it returns the merge_id); min_confidence and limit off-spec values degrade to their defaults rather than erroring.

5. Undo — revert within the 30-minute window

Every merge — auto-run, contacts/merge, or a review-queue accept — returns a merge_id (the guide Identity resolution shows it on the merge response). While the window is open, one call restores the split:
After the window closes, the surviving audit row in GET /api/v1/contacts/merge-history governs — an expired undo returns 404/422 with a pointer to the support unmerge path. The guide Identity resolution covers that hold and the per-field survivorship the restore applies.

6. Node SDK — preview + run in one CodeGroup

The same cycle from the Node SDK once it ships the CDP surface; until then call the endpoints over fetch — or use the SDK’s generic request helper:
The per-endpoint schemas — query params, request bodies, and the full response shape for every call named here — live in the CDP API reference.

Cross-references