> ## 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.

# Merge workspace console: scan, pick fields, and fold duplicate contacts

> Operate the Audience → Merge duplicates workspace end to end — run the exact or fuzzy duplicate scan, pick each conflicting field in the side-by-side picker, understand what the merge carries forward, and recover from non-blocking errors without losing the survivor.

# Merge workspace console

**Audience → Merge duplicates** (`/audience/merge`) is the deterministic merge workspace: it scans your contact base for duplicate groups, shows each group as a side-by-side record comparison, and folds the duplicates into one survivor with a per-field picker in front of you. It is the page you open after an import, a federation sync, or any ingest path that can land the same person as two rows.

The workspace is the operator console over the exact/fuzzy duplicate scan. The sibling **Identity resolution** console (`/audience/identity-resolution`) is the probabilistic queue — candidates the engine scored on corroborating signals rather than a shared identifier. This page links across whenever its own scan comes up clean but the engine still holds candidates, so "no duplicates here" never reads as "nothing pending anywhere." For the full lifecycle across both surfaces, see the [identity resolution and merge walkthrough](/guides/identity-resolution-and-merge-walkthrough); for the per-pair API runbook, see [contact merge end-to-end](/guides/contact-merge).

## 1. What the merge workspace is

The page runs a four-card loop:

* **Stats** — total contacts loaded, duplicate groups found in the latest scan, and merges recorded in history. The counts update after every scan and merge.

* **Duplicate detection** — the scan card. Two buttons and a strategy selector:

  * **Quick Scan (first 100)** groups contacts locally over the first page of your contact list. Use it for a fast spot-check; it cannot see the rest of the tenant.
  * **Scan All Contacts** runs the server-side scan (`GET /api/v1/contacts/duplicates`) over the full tenant and runs automatically when the page opens.
  * **Detection strategy** picks how the server groups:
    * **Exact** — records group when their normalized email or normalized phone matches. Phone numbers normalize to digits before grouping, so `+1 415 555 1234` and `+14155551234` land in the same group. High precision, conservative recall.
    * **Fuzzy** — exact grouping plus a name-similarity pass inside the same area code or email domain. A **Similarity threshold** slider (0.70–0.95, default 0.80) sets the floor: drop toward 0.70 for more recall, raise toward 0.90+ when false-positive merges are expensive. The page re-scans the moment you switch strategy or commit a new threshold.

  Each found group renders as a card listing its member records and the field they matched on, with a per-group confidence on fuzzy matches.

* **Merge dialog** — the two-record merge UI. Clicking **Merge** on a group opens a dialog that lines the records up field by field: for every field where the records disagree, you pick which record's value survives (a radio per row per field). Fields where every record agrees, or where only one record carries a value, do not ask. The dialog pre-selects the most complete value per field — the record holding the longest non-empty value — so a clean pair usually needs no clicks. The first record in the group is always the primary: its contact id survives, and external references to it (webhooks, integrations, segments) keep resolving.

* **Merge history** — every completed merge, newest first: when, which contact survived, the match type, and how many records were folded in. The list is backed by the audit log, so it survives a refresh and shows merges other operators ran. Each row offers **Undo merge** for 30 minutes after the merge; the button disappears when the window closes.

## 2. Where it lives

The workspace is a dashboard page served at `/audience/merge` in the app router (`apps/web/src/app/[locale]/(dashboard)/audience/merge/page.tsx`), next to the other Audience consoles. It renders for the seats that can see PII, and links back to **Audience → Contacts** and forward to the identity resolution console from its header.

Every action on the page runs over the public API — nothing here is privileged over an API caller:

| Action in the workspace | Endpoint |
| - | - |
| Scan All Contacts (exact or fuzzy) | `GET /api/v1/contacts/duplicates?strategy=exact\|fuzzy&threshold=0.80` |
| Merge a group (one call per folded-in record) | `POST /api/v1/contacts/merge` |
| Merge history list | `GET /api/v1/contacts/merge-history` |
| Undo inside the 30-minute window | `POST /api/v1/contacts/unmerge/:mergeId` |

## 3. Which fields inherit, which are pick-one

The merge treats contact data in three classes:

**Pick-one fields (scalar survivorship).** The dialog's per-field radio covers the scalar identity fields. You decide each one per merge; the tenant survivorship policy fills in any field you do not pick, and the merge's global strategy is the last fallback:

`phone`, `email`, `whatsapp_id`, `viber_id`, `first_name`, `last_name`, `display_name`, `company`, `country_code`, `timezone`, `language`, `external_id`

**Union fields (merge automatically, no picker).** Some data combines rather than competes:

* **Tags** union — the survivor keeps every tag from every record. The dialog may show a tags row for comparison, but tags always union server-side; there is no pick-one decision to make.
* **Channel preferences and custom attributes** merge key-by-key — a key only one record carried is preserved on the survivor.
* **Message counters** (`total_messages_sent`, `total_messages_received`) add up across the folded records.
* **Channel memberships** carry forward — the survivor stays subscribed everywhere either record was.

**Carried-forward history (re-pointed, not copied).** The folded record's conversations, messages, call logs, and list memberships re-point to the survivor's id. Nothing is duplicated; the history moves wholesale onto the surviving profile, and Customer-360 reads it there after the merge.

## 4. Cascading effects

A merge is small on the contacts table and wide everywhere else. Plan for four waves:

* **Subscription and consent state.** Consent and opt-outs are tenant-owned and follow the person, not the row: the survivor inherits every recorded grant or revocation from both sides, and a revocation recorded on the folded record continues to suppress the survivor on that channel. Verify the survivor against the consent check (`POST /api/v1/cdp/consent/check`) before the next campaign send — do not read either old record's raw flags. See the [consent inspector](/guides/audience-consent-inspector) and [opt-outs console](/guides/audience-opt-outs-console).
* **Conversation history.** Open and historical conversations re-point to the survivor, so the inbox threads and the Customer-360 timeline read continuously off one profile. The undo path restores the original attribution inside the window.
* **Segments and scores.** Segment membership, computed traits, and model scores keyed on the folded ids resolve to the survivor after the fold. A static segment saved before the merge still holds the folded id — re-sync or re-save the segment if it must reflect the survivor's id exactly.
* **Account relations (B2B).** When the contact sits in golden accounts, the contact→account membership now names the survivor. The account's own golden record is untouched. See [account relations](/guides/audience-accounts-relations).

A wrong merge corrupts all four waves at once, which is why the dry decision — is this pair really one person — belongs before the dialog, not after. When a pair is ambiguous, leave it in the [identity resolution](/guides/identity-resolution) review queue instead of merging on a hunch.

## 5. Non-blocking errors and retries

The workspace is built so a partial failure never reads as a clean database:

* **Contacts failed to load.** If the contact list read fails, the page shows a retry card — it will not fall through to "No duplicates found" over a list that never loaded. Press **Retry** to re-fetch, then re-scan.
* **Server scan failed.** If the full-tenant scan errors, the page tells you and the local **Quick Scan** stays available as the fallback path.
* **Partial group merge.** A group merge issues one merge call per folded-in record. If one record fails and the others succeed, a warning names the partial result, the failed records stay in the group (and in your contact list), and the group is **not** cleared — reopen it and merge the remainder. Nothing about a partial failure blocks the next attempt or the next group.
* **Undo gates.** **Undo merge** works inside a 30-minute window and exactly once. Outside the window the server answers `UNDO_EXPIRED`; a second undo on the same merge answers `UNDO_ALREADY_USED`. The workspace maps both to a plain-language toast and re-syncs the history list, so an expired button never lingers. After the window the fold is durable — contact support to unwind it.

Every merge, unmerge, and failure lands in the audit log with the acting user, so a lost race between two operators is always reconstructible from **Merge history** and the [audit log](/guides/audit-log).

## 6. Example: merge an email-only and a phone-only contact after federation sync

An A2A federation sync (see [A2A federation setup](/guides/a2a-federation-setup)) upserts partner-referenced contacts keyed on email. Your SMS traffic already created the same person keyed on phone. The sync lands and you now hold two rows: `ava@acme.com` with no phone, and `+14155551234` with no email. Neither shares the other's identifier, so the deterministic rules could not fold them at ingest.

1. Open **Audience → Merge duplicates**. The page runs **Scan All Contacts** on load.
2. Switch **Detection strategy** to **Fuzzy**. If the pair stays ungrouped at the default 0.80 — different display names, say — lower the **Similarity threshold** toward 0.70 and let the re-scan run. The pair groups when name similarity corroborates.
3. Open the group's **Merge** dialog. The two records line up: one row carries the email, the other the phone, and the names disagree slightly (`Ava Chen` vs `Ava`).
4. Field by field, the picks are the union you actually want:
   * `email`: pick the federation-synced record's value — the only one present, pre-selected.
   * `phone`: pick the SMS-created record's value — the only one present, pre-selected.
   * `display_name`: genuinely conflicting — pick `Ava Chen` from the record you trust more. This is the one decision the scan cannot make for you.
5. Confirm. The primary record keeps its id and absorbs the phone, the email stays, tags union, and the SMS record's conversation history re-points to the survivor. The fold lands in **Merge history** with a 30-minute **Undo merge** button.
6. Before the next campaign send, run the consent check on the survivor — an opt-out recorded against the SMS record now suppresses the merged profile on SMS, exactly as it should.

If the pair never groups even at 0.70, it is not a deterministic duplicate — check the [identity resolution console](/guides/identity-resolution), where the probabilistic engine scores pairs that share no exact identifier at all.

## See also

* [Identity resolution and merge walkthrough](/guides/identity-resolution-and-merge-walkthrough) — the ten-surface lifecycle this console sits inside
* [Contact merge end-to-end](/guides/contact-merge) — the per-pair runbook with the full merge API contract
* [Identity resolution](/guides/identity-resolution) — the probabilistic review queue for pairs this scan cannot find
* [Merge policy concept](/concepts/contact-merge-policy) — the survivorship rules the per-field picks override
* [Customer-360 workspace](/guides/customer-360-workspace) — confirm the survivor reads right after the fold
* [A2A federation setup](/guides/a2a-federation-setup) — the sync that seeds cross-identifier duplicates
* [Audience hub orientation](/guides/audience-hub-orientation) — where Merge duplicates sits among the audience consoles


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