> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Identity resolution and merge: end-to-end walkthrough

> Run the full identity lifecycle in order — ingest, dedupe, preview, merge, survivorship, and the Customer-360 read — with the decision tree that picks the right surface per duplicate shape, worked examples per step, and the edge cases (soft-deletes, suppression, PII scoping, consent) that gate a merge.

# Identity resolution and merge: end-to-end walkthrough

The individual guides — [identity resolution](/guides/identity-resolution), [contact merge](/guides/contact-merge), the [survivorship policy concept](/concepts/contact-merge-policy), the [Customer-360 workspace](/guides/customer-360-workspace), [anonymous identity stitching](/guides/anonymous-identity-stitching), and [account relations](/guides/audience-accounts-relations) — each go deep on one surface. This walkthrough strings them into the one operator loop that keeps a golden profile golden: ingest with identifiers, scan for duplicates, preview the merge, apply it with a survivorship plan, and confirm the survivor reads right in Customer-360.

Run the steps in this order. Identity work downstream of segments is the expensive order; identity work before segments is the cheap one.

## 1. Why identity precedes ingestion

One person should produce one golden profile. When ingestion races ahead of identity rules, the same person lands as two contact rows — a web signup keyed on email, an inbound SMS keyed on phone — and cleaning that up later is *merge debt*: every segment built on the dirty base counted them twice, every campaign that sent to the twin has delivery history you now have to reconcile, and every opt-out recorded on one row was reachable through the other. The cheaper order is deterministic rules first ([step 2](#2-ingest-with-the-dedupe-surfaces-in-mind)), then the review queue clears what the rules can-not settle, then segments and scores build on the single profile.

## 2. Ingest with the dedupe surfaces in mind

Identity resolution can only match over the identifiers your contacts actually carry, so decide which dedupe surface resolves new identifiers BEFORE the first import or SDK call:

**Deterministic rules are the always-on layer.** `PUT /api/v1/cdp/identity-rules` authors one rule per identifier type (`external_id`, `email`, `phone`, `anonymous_id`) with a priority; ingestion resolves to an existing contact on the strongest enabled rule the moment an identifier arrives. Prefer your own system's stable key in `external_id` when one exists — it beats phone and email formats:

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/cdp/identity-rules \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "match_type": "external_id", "priority": 1, "enabled": true }'
```

Feeding identifiers at contact creation is the second half of the same decision — pass phone, email, and `external_id` on `POST /api/v1/contacts`, on imports, and on webhook-driven upserts:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/contacts \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Ava",
    "last_name": "Chen",
    "email": "ava@acme.com",
    "phone": "+14155551234",
    "external_id": "crm-44882"
  }'
```

A contact created with the three identifiers above is the **anchor record** the rest of this walkthrough folds duplicates into. Its `profileId` (`cnt_01f3…`) never changes across merges — pick the record your external systems already reference as the survivor, not the oldest row.

## 3. Map the ten dedupe and merge surfaces

The Audience hub splits identity across ten consoles; knowing which one answers which duplicate shape is the first operator skill. The dashboard routes sit under `/audience` (see the [Audience hub orientation](/guides/audience-hub-orientation) for the full tile map):

| Surface | Route | What it does | Deep dive |
| - | - | - | - |
| **Identity resolution** | `/audience/identity-resolution` | Probabilistic review queue with confidence bands and a side-by-side diff per candidate | [Identity resolution guide](/guides/identity-resolution) |
| **Merge** | `/audience/merge` | Deterministic exact/fuzzy duplicate scan, merge dialog, merge history | [Contact merge end-to-end](/guides/contact-merge) |
| **Survivorship policy** | onboard from Identity resolution | Tenant-wide per-field precedence rules every merge honors | [Merge policy concept](/concepts/contact-merge-policy) |
| **Customer-360** | `/audience/contacts/<id>/360` | The survivor's post-merge picture — conversations, calls, CRM, journeys | [Customer-360 workspace](/guides/customer-360-workspace) |
| **Anonymous identity stitch** | `POST /sdk/identify` or `POST /api/v1/cdp/identity-stitch` | Binds anonymous sessions to a known contact, real-time or late-declared | [Anonymous stitching guide](/guides/anonymous-identity-stitching) |
| **Accounts (B2B)** | `/audience/accounts` | Golden-account relations — memberships, hierarchy, person↔company | [Account relations guide](/guides/audience-accounts-relations) |
| **Rules simulator** | Identity resolution page | What-if preview of a proposed rule before it goes live | [CDP identity simulation](/guides/cdp-identity-simulation) |
| **Confidence bands concept** | engine model | How the engine bands candidates into auto-merge, review, dismiss | [Identity resolution model](/concepts/cdp-identity-resolution) |
| **Contacts (360/Journey)** | `/audience/contacts` | The read surface segments and scores resolve to after the fold | [Customer-360 workspace](/guides/customer-360-workspace) |
| **Opt-outs + Consent** | `/audience/opt-outs`, `/audience/consent` | Gate the merged record before any campaign sends | [Opt-outs](/guides/audience-opt-outs-console), [Consent inspector](/guides/audience-consent-inspector) |

## 4. Decision tree — which surface picks up a duplicate

A new duplicate row needs one of three resolutions, and the tree is stable across the ten surfaces:

```text theme={null}
Is the pair's proven identifier the same (normalized email, normalized
phone, or external_id)?
  yes → deterministic surface: Merge duplicates (/audience/merge),
        exact strategy; the pair folds without a human call.
  no  →
    Do the two records corroborate under fuzzy comparison
    (name similarity, respelled phone, shared channel id)?
      yes → probabilistic surface: Identity resolution
            (/audience/identity-resolution); the pair lands in the
            review band and a steward decides each one.
      no  → leave the pair alone. A shared handset, a reassigned
            number, or a family email is a false positive the
            review queue must reject, not a merge candidate.
```

When a candidate could go either way, default to the review queue. Rejecting a bad pair costs a re-scan; accepting a bad merge folds one person into somebody else's record and corrupts every downstream reader at once.

## 5. Configure identity resolution — rules, review, and bands

The [identity resolution guide](/guides/identity-resolution) is the deep dive; the operator walk is short:

1. **Enable the deterministic rule per identifier.** The example in [step 2](#2-ingest-with-the-dedupe-surfaces-in-mind) enabled `external_id` at priority 1. Repeat for `email` and `phone` at lower priorities so the strongest signal wins first.
2. **Simulate before enabling.** `POST /api/v1/cdp/identity-resolution/simulate` returns the what-if preview — which pairs the proposed rule would merge, the per-field survivorship preview, and an over-merge warning when a rule would collapse clearly-different people. Run this against the full policy before it goes live.
3. **Let the scan clear the residue.** `GET /api/v1/cdp/identity/merge-candidates` with `include_auto_merge=false` returns only the review band — the ambiguous remainder the rules could not settle. Bound the scan with `min_confidence` (0–100) when a noisy import fills the queue.

Each candidate carries a confidence score and a band:

* **High confidence (`auto_merge`)** — prepared to merge automatically, for example normalized phone and email corroborating each other.
* **Review band (`review`)** — a steward confirms each pair before it merges; a phone-only link whose members carry different emails lands here by design.

## 6. Read the side-by-side diff in the Merge queue

Open **Audience → Merge duplicates** (`/audience/merge`). The scan finds duplicate groups with either strategy:

```bash theme={null}
# Exact: normalized email + normalized phone grouping
curl -X GET "https://api.orbit.devotel.io/api/v1/contacts/duplicates?strategy=exact" \
  -H "X-API-Key: dv_live_sk_your_key_here"

# Fuzzy: exact + name-similarity, threshold 0.70–0.95 (default 0.80)
curl -X GET "https://api.orbit.devotel.io/api/v1/contacts/duplicates?strategy=fuzzy&threshold=0.85" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Each group renders as a side-by-side diff: the two records' scalar fields (name, email, phone, WhatsApp/Viber ids, company, country, timezone, language, external id) line up per row, and the custom-field surface shows the same per-key split. The candidate below — anchor record `cnt_01f3…` vs. an import twin `cnt_02a8…` — shows a confidence of 91 and matched fields `phone` and `email`, so the diff reads like the anchor's row with a second column:

| Field | Primary (`cnt_01f3…`) | Candidate (`cnt_02a8…`) | Survivorship pick |
| - | - | - | - |
| `email` | [ava@acme.com](mailto:ava@acme.com) | [ava@acme.com](mailto:ava@acme.com) | primary (equal) |
| `phone` | +14155551234 | +14155551234 | primary (equal) |
| `external_id` | crm-44882 | *(null)* | pin `primary_wins` |
| `display_name` | Ava Chen | Ava (web form) | pin `primary_wins` |
| `timezone` | America/New\_York | *(null)* | policy `prefer_non_null` |
| `company` | Acme | *(null)* | policy `prefer_non_null` |

The decision per conflicting field is the survivorship rule the pair honors; equal values need none, null-vs-value fills under `prefer_non_null`, and the genuinely conflicting fields (`display_name` here) need a pin or a tenant-policy rule.

## 7. Work the survivorship policy

The tenant survivorship policy — authored on the Identity resolution page or `GET`/`PUT`/`DELETE` `/api/v1/cdp/survivorship-policy` — fills in every field you do not pin per merge. Three strategies cover the worked-example cases:

**Last-touch wins (`most_recently_updated`).** Take the value from the record with the newer `updated_at`. Use it when the freshest write is the most trustworthy — a web-preference form the person just filled beats a stale CRM row.

**First-touch wins (`most_recently_created` inverted, or `prefer_target` where the target is the older record).** Use it when the original registration is canonical and later imports polluted the row.

**Provenance wins (`prefer_source_system`).** Prefer whichever record's `source` column matches a named system (`crm`, `salesforce`, `import`) — the strategy to reach for when "our CRM is the source of truth for name and company."

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/cdp/survivorship-policy \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      { "field": "email", "strategy": "prefer_source_system", "source_system": "crm" },
      { "field": "phone", "strategy": "prefer_non_null" }
    ],
    "default_strategy": "most_recently_updated"
  }'
```

The governed fields are a closed set — the scalar contact columns the merge actually honors: `phone`, `email`, `whatsapp_id`, `viber_id`, `first_name`, `last_name`, `display_name`, `company`, `country_code`, `timezone`, `language`, `external_id`. Channel memberships, tags, conversation history, and CDP events fold in as unions with no pin available; the per-field decision applies only to scalars. Timestamps conflicts settle per field and per rule — run the [rules simulator](/guides/identity-resolution) against the full policy before enabling it, so a last-touch rule whose timestamps disagree catches the over-merge warning rather than sweeping two different people.

## 8. Write the merge

POST the pair with the survivorship plan from [step 6](#6-read-the-side-by-side-diff-in-the-merge-queue). A per-request `fieldStrategies` pin beats the tenant policy, and any unpinned field falls to the policy then to `mergeStrategy`:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/contacts/merge \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "primaryId": "cnt_01f3…",
    "secondaryIds": ["cnt_02a8…"],
    "mergeStrategy": "primary_wins",
    "fieldStrategies": {
      "display_name": "primary_wins",
      "external_id": "primary_wins",
      "timezone": "secondary_wins"
    }
  }'
```

The response returns the survivor and a `merge_id`:

```json theme={null}
{
  "surviving_contact_id": "cnt_01f3…",
  "merge_id": "mrg_8f3c2b1d",
  "folded_ids": ["cnt_02a8…"],
  "undo_window_seconds": 1800
}
```

(`surviving_source` is the equivalent label when you author the merge payload against the concept's naming — the field names on the wire are `primaryId` / `secondaryIds`; inside the survivorship policy the same pair reads as target/source.) The 30-minute `undo_window` is the rollback path: `POST /api/v1/contacts/unmerge/:mergeId` reverts the fold inside the window; `GET /api/v1/contacts/merge-history` keeps the audit row (survivor, folded-in ids, acting user, strategy) forever. After the window the fold is durable.

## 9. Confirm the survivor in Customer-360

The merge resolves **identity**; Customer-360 confirms every downstream reader picked the survivor up. Open **Audience → Contacts → `<id>/360`** or call the snapshot endpoint:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/customer-360/contacts/cnt_01f3…/snapshot" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Expect the folded-in record's conversations, calls, and events to now read on the survivor — the endpoint fans every source in parallel and returns the full envelope, so a missing section is an empty array rather than a failed read. When segments or personalization reference the folded-in ids, point them at the survivor id before you trust the base. See the [Customer-360 workspace guide](/guides/customer-360-workspace) for the full envelope map.

## 10. Edge cases that gate every merge

**Soft-deleted and merged-away rows.** A folded-in record stops being reachable as a separate profile, but a re-imported identical contact re-enters the duplicates queue — run the scan after every bulk import so a re-imported twin lands in the review band, not back in segments.

**Suppressed contacts.** A fold does not clear suppression. When either side of the pair carried an opt-out, verify the survivor against `GET /api/v1/cdp/consent/:contactId` and `POST /api/v1/cdp/consent/check` before any campaign sends — the authoritative consent answer, not either record's raw flags.

**PII scoping.** The identity, merge, and Customer-360 surfaces render only for owner, admin, and developer seats; a viewer sees none of the tiles, and the destination page 403s them regardless. The [Audience hub orientation](/guides/audience-hub-orientation) maps the permission scopes per tile.

**Consent-aware merging.** Consent is tenant-owned and survives the fold — the survivor inherits every recorded grant or revocation from both sides, and a revocation on the folded-in record continues to suppress the survivor on that channel. For the auditor-facing export, see [export consent and suppression](/compliance/consent-suppression-export).

**B2B account relations.** When the contact is a member of golden accounts (`/audience/accounts`), the fold updates the contact↔account membership — the account's golden record does not change, but the Members list now names the survivor. Re-check the account's detail pane when the merged contact appears under multiple relations. See [account relations](/guides/audience-accounts-relations).

## See also

* [Identity resolution guide](/guides/identity-resolution) — deterministic rules, review queue, simulation
* [Contact merge end-to-end](/guides/contact-merge) — the per-pair operator runbook
* [Merge policy concept](/concepts/contact-merge-policy) — the decision tree the survivorship policy implements
* [Customer-360 workspace](/guides/customer-360-workspace) — the survivor's read surface
* [Anonymous identity stitching](/guides/anonymous-identity-stitching) — the pre-identify seam that feeds the same pipeline
* [Account relations](/guides/audience-accounts-relations) — golden-account memberships after a fold
* [Audience hub orientation](/guides/audience-hub-orientation) — the tile map every surface above links from


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.