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
emailplusphonepair this-and-that corroborate each other, so equality, not similarity, does the match.POST /api/v1/cdp/identity-resolution/previewdry-runs the groups without writing;POST /api/v1/cdp/identity-resolution/runauto-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-candidatesreturns ranked pairs with a confidence and a band;min_confidenceandinclude_auto_merge=falsebound 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.
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:- Per-request pins (
fieldStrategies, plusattribute.<key>pins for custom fields) — set on the merge call itself, highest precedence. - 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 areprefer_target/prefer_source,most_recently_updated/most_recently_created,prefer_non_null, andprefer_source_system(value from the record whose source is a named system —crm,salesforce,import). - The safe default — blanks and nulls never overwrite a populated value; a partial update cannot erase an established trait.
- Reversal is bounded, not vague. Every merge returns a
merge_id; the undo window stays open for 30 minutes, andPOST /api/v1/contacts/unmerge/:mergeIdrestores the split while it is open.GET /api/v1/contacts/merge-historyis 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.
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.
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-candidatesor 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.
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.
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.
auto_apply; the rest form
review_queue. Reason values are masked because the default PII posture is
mask-on-return:
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 theauto_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.
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.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 withinclude_auto_merge=false and a min_confidence
(0–100) floor:
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:
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 overfetch — or use the SDK’s generic request helper:
Cross-references
- Identity resolution (guide) — the operational queue, console, and undo flow.
- The CDP event model — the identity-hint envelope upstream of resolution.
- How CDP segments recompute — the membership cadence a merge feeds.
- CDP API reference — the endpoint-level surface of every call named above.