From 0f64c17d2128fd33d55cbfc866536a6cdc4095db Mon Sep 17 00:00:00 2001 From: Marc <97827744+marcmlc@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:31:57 +0100 Subject: [PATCH 1/7] Add PreviewMergeContact to the Preview spec Co-Authored-By: Claude Opus 4.8 (1M context) --- descriptions/0/api.intercom.io.yaml | 238 ++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/descriptions/0/api.intercom.io.yaml b/descriptions/0/api.intercom.io.yaml index 816603d..4b00e3d 100644 --- a/descriptions/0/api.intercom.io.yaml +++ b/descriptions/0/api.intercom.io.yaml @@ -8233,6 +8233,110 @@ paths: from: 6762f0d51bb69f9f2193bb7f into: 6762f0d51bb69f9f2193bb80 skip_duplicate_validation: true + "/contacts/merge/preview": + post: + summary: Preview a contact merge + parameters: + - name: Intercom-Version + in: header + schema: + "$ref": "#/components/schemas/intercom_version" + tags: + - Contacts + operationId: PreviewMergeContact + description: | + Preview the result of merging one contact into another **before** running the merge. This is a read-only dry run: it reports whether the merge is allowed and, if so, exactly what would change — without modifying any data. + + Send the same `from` (a `lead`) and `into` (a `user`) contact IDs you would pass to [Merge a lead and a user](/docs/references/preview/rest-api/api.intercom.io/contacts/mergecontact). + + - When `allowed` is `true`, the response contains a `summary`: every attribute change (with the resulting value on the surviving contact) and the number of conversations, notes, and tags that would be reassigned. + - When `allowed` is `false`, the response contains `reasons`. Each reason has a stable `code` you can branch on and a human-readable `message` you can display. + + {% admonition type="warning" name="Previewing non-duplicate contacts" %} + By default a merge requires the two contacts to share an identifier (email, phone, or external ID). Set `skip_duplicate_validation` to `true` to preview a merge of contacts that don't — the same rule the merge endpoint enforces. + {% /admonition %} + + When `allowed` is `false`, `reasons[].code` is one of: + + | code | meaning | + | --- | --- | + | `invalid_merge_role_pair` | The `from`/`into` roles aren't mergeable. `from` must be a lead and `into` a user. | + | `no_shared_identifier` | The contacts share no email, phone, or external ID. Set `skip_duplicate_validation` to preview anyway. | + | `facebook_user` | Facebook contacts can't be merged. | + | `external_lead_channel_not_allowed` | The source is an external-channel lead that isn't eligible for merging. | + | `mailing_list` | Contacts created from a mailing list can't be merged. | + | `source_is_user` | The source (`from`) is a user; only a lead can be merged into a user. | + | `user_to_lead` | A user can only be merged into another user, not a lead or visitor. | + responses: + '200': + description: successful + content: + application/json: + examples: + allowed: + summary: Merge allowed + value: + allowed: true + summary: + attribute_changes: + attributes: + - field: name + outcome: adopted + from_value: Jo + new_into_value: Jo + - field: companies + outcome: merged + from_value: + - Beta + into_value: + - Acme + new_into_value: + - Acme + - Beta + - field: phone + outcome: discarded + from_value: "+1555" + into_value: "+1999" + new_into_value: "+1999" + total_count: 3 + reassignments: + conversations: 14 + tags: 3 + notes: 2 + blocked: + summary: Merge not allowed + value: + allowed: false + reasons: + - code: no_shared_identifier + message: These contacts don't share a common identifier (email, phone, or external ID), so they aren't recognised as duplicates. Set skip_duplicate_validation to true to preview the merge anyway. + schema: + "$ref": "#/components/schemas/merge_preview_response" + '401': + description: Unauthorized + content: + application/json: + examples: + Unauthorized: + value: + type: error.list + request_id: ff328c7c-6140-48eb-84dd-bb8960b66cd0 + errors: + - code: unauthorized + message: Access Token Invalid + schema: + "$ref": "#/components/schemas/error" + requestBody: + content: + application/json: + schema: + "$ref": "#/components/schemas/merge_preview_request" + examples: + allowed: + summary: successful + value: + from: 6762f0d51bb69f9f2193bb7f + into: 6762f0d51bb69f9f2193bb80 "/contacts/search": post: summary: Search contacts @@ -34635,6 +34739,140 @@ components: description: Set to `true` to merge two contacts that are not duplicates (they share no matching email or phone). example: true + merge_preview_request: + description: Preview a contact merge. + type: object + title: Merge preview request + properties: + from: + type: string + description: The unique identifier for the contact to merge away from. Must be a lead. + example: 6762f0d51bb69f9f2193bb7f + into: + type: string + description: The unique identifier for the contact to merge into. Must be a user. + example: 6762f0d51bb69f9f2193bb80 + skip_duplicate_validation: + type: boolean + default: false + description: When true, previews the merge even if the contacts don't share a common identifier. Defaults to false. + merge_preview_response: + title: Merge preview response + description: The result of a merge preview. Either the merge is allowed (with a summary of the changes) or it is not (with reasons). + oneOf: + - "$ref": "#/components/schemas/merge_preview_allowed" + - "$ref": "#/components/schemas/merge_preview_blocked" + merge_preview_allowed: + title: Merge preview allowed + type: object + description: Returned when the merge is allowed. + properties: + allowed: + type: boolean + enum: + - true + description: Always true. The merge is allowed. + summary: + "$ref": "#/components/schemas/merge_preview_summary" + merge_preview_blocked: + title: Merge preview not allowed + type: object + description: Returned when the merge is not allowed. + properties: + allowed: + type: boolean + enum: + - false + description: Always false. The merge is not allowed. + reasons: + type: array + description: One or more reasons the merge is not allowed. + items: + "$ref": "#/components/schemas/merge_preview_reason" + merge_preview_reason: + title: Merge preview reason + type: object + description: A reason a merge is not allowed. + properties: + code: + type: string + description: A stable code identifying the reason. Branch on this value. + enum: + - invalid_merge_role_pair + - no_shared_identifier + - facebook_user + - external_lead_channel_not_allowed + - mailing_list + - source_is_user + - user_to_lead + message: + type: string + description: A human-readable explanation. Display this to people; do not match on it, as the wording may change. + merge_preview_summary: + title: Merge preview summary + type: object + description: A summary of the changes the merge would make to the surviving contact. + properties: + attribute_changes: + "$ref": "#/components/schemas/merge_preview_attribute_changes" + reassignments: + "$ref": "#/components/schemas/merge_preview_reassignments" + merge_preview_attribute_changes: + title: Merge preview attribute changes + type: object + description: The attribute changes the merge would make to the surviving contact. + properties: + attributes: + type: array + description: The individual attribute changes. Capped at 50 items; total_count reflects the full number of changes. + items: + "$ref": "#/components/schemas/merge_preview_attribute_change" + total_count: + type: integer + description: The total number of attribute changes detected, which may exceed the number of items in attributes. + example: 3 + merge_preview_attribute_change: + title: Merge preview attribute change + type: object + description: A single attribute change on the surviving contact. + properties: + field: + type: string + description: The contact field that would change. + example: name + outcome: + type: string + description: How the value is resolved. adopted = the source's value is taken; discarded = the source's value is dropped and the target's kept; merged = the two values are combined. + enum: + - adopted + - discarded + - merged + from_value: + nullable: true + description: The source contact's value. Omitted when null. May be a string, array, or object depending on the field. + into_value: + nullable: true + description: The target contact's value. Omitted when null. May be a string, array, or object depending on the field. + new_into_value: + nullable: true + description: The resulting value on the surviving contact. Always present; may be null. May be a string, array, or object depending on the field. + merge_preview_reassignments: + title: Merge preview reassignments + type: object + description: The number of records that would be reassigned to the surviving contact, by type. Only types with reassignments are included. + properties: + conversations: + type: integer + description: The number of conversations that would be reassigned. + example: 14 + tags: + type: integer + description: The number of tags that would be reassigned. + example: 3 + notes: + type: integer + description: The number of notes that would be reassigned. + example: 2 merge_conversations_request: title: Merge Conversations Request type: object From ce7c22ce2dd8111c8ff0f6eae57d50a34b9f426f Mon Sep 17 00:00:00 2001 From: Marc <97827744+marcmlc@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:12:02 +0100 Subject: [PATCH 2/7] Reword merge preview description (drop em dash, clarify dry run) Co-Authored-By: Claude Opus 4.8 (1M context) --- descriptions/0/api.intercom.io.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/descriptions/0/api.intercom.io.yaml b/descriptions/0/api.intercom.io.yaml index 4b00e3d..5d94418 100644 --- a/descriptions/0/api.intercom.io.yaml +++ b/descriptions/0/api.intercom.io.yaml @@ -8245,7 +8245,7 @@ paths: - Contacts operationId: PreviewMergeContact description: | - Preview the result of merging one contact into another **before** running the merge. This is a read-only dry run: it reports whether the merge is allowed and, if so, exactly what would change — without modifying any data. + Preview the result of merging one contact into another without applying the merge. This is a read-only dry run: it reports whether the merge is allowed and, if so, exactly what would change, **without modifying any data**. Send the same `from` (a `lead`) and `into` (a `user`) contact IDs you would pass to [Merge a lead and a user](/docs/references/preview/rest-api/api.intercom.io/contacts/mergecontact). From 52405b27a0653bc2875110a4de7642614a229211 Mon Sep 17 00:00:00 2001 From: Marc <97827744+marcmlc@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:29:49 +0100 Subject: [PATCH 3/7] Drop user_to_lead from merge preview reasons (u2u not officially supported) Co-Authored-By: Claude Opus 4.8 (1M context) --- descriptions/0/api.intercom.io.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/descriptions/0/api.intercom.io.yaml b/descriptions/0/api.intercom.io.yaml index 5d94418..4631c65 100644 --- a/descriptions/0/api.intercom.io.yaml +++ b/descriptions/0/api.intercom.io.yaml @@ -8266,7 +8266,6 @@ paths: | `external_lead_channel_not_allowed` | The source is an external-channel lead that isn't eligible for merging. | | `mailing_list` | Contacts created from a mailing list can't be merged. | | `source_is_user` | The source (`from`) is a user; only a lead can be merged into a user. | - | `user_to_lead` | A user can only be merged into another user, not a lead or visitor. | responses: '200': description: successful @@ -34804,7 +34803,6 @@ components: - external_lead_channel_not_allowed - mailing_list - source_is_user - - user_to_lead message: type: string description: A human-readable explanation. Display this to people; do not match on it, as the wording may change. From 31b1dfa14f3c249cf3c3b5fb13b686f7987393d0 Mon Sep 17 00:00:00 2001 From: Marc <97827744+marcmlc@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:33:42 +0100 Subject: [PATCH 4/7] Drop source_is_user from merge preview reasons (shadowed by invalid_merge_role_pair) Co-Authored-By: Claude Opus 4.8 (1M context) --- descriptions/0/api.intercom.io.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/descriptions/0/api.intercom.io.yaml b/descriptions/0/api.intercom.io.yaml index 4631c65..ead7288 100644 --- a/descriptions/0/api.intercom.io.yaml +++ b/descriptions/0/api.intercom.io.yaml @@ -8265,7 +8265,6 @@ paths: | `facebook_user` | Facebook contacts can't be merged. | | `external_lead_channel_not_allowed` | The source is an external-channel lead that isn't eligible for merging. | | `mailing_list` | Contacts created from a mailing list can't be merged. | - | `source_is_user` | The source (`from`) is a user; only a lead can be merged into a user. | responses: '200': description: successful @@ -34802,7 +34801,6 @@ components: - facebook_user - external_lead_channel_not_allowed - mailing_list - - source_is_user message: type: string description: A human-readable explanation. Display this to people; do not match on it, as the wording may change. From 62eb6cce880fef3a5cc631ee069a1c46e35d9460 Mon Sep 17 00:00:00 2001 From: Marc <97827744+marcmlc@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:14:47 +0100 Subject: [PATCH 5/7] Make from and into required on merge_preview_request Co-Authored-By: Claude Opus 4.8 (1M context) --- descriptions/0/api.intercom.io.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/descriptions/0/api.intercom.io.yaml b/descriptions/0/api.intercom.io.yaml index ead7288..bdf6fb9 100644 --- a/descriptions/0/api.intercom.io.yaml +++ b/descriptions/0/api.intercom.io.yaml @@ -34741,6 +34741,9 @@ components: description: Preview a contact merge. type: object title: Merge preview request + required: + - from + - into properties: from: type: string From 81d7b6b5c14a6c7e39cf5fdea2a7abaad6fa2def Mon Sep 17 00:00:00 2001 From: Marc <97827744+marcmlc@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:31:24 +0100 Subject: [PATCH 6/7] Split conversation reassignments into owned and participated in merge preview Mirror intercom/intercom#536857: the merge preview response now reports owned_conversations (primary-user ownership transfer) and participated_conversations (participation move) instead of a single conversations count. Co-Authored-By: Claude Opus 4.8 (1M context) --- descriptions/0/api.intercom.io.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/descriptions/0/api.intercom.io.yaml b/descriptions/0/api.intercom.io.yaml index bdf6fb9..0214d86 100644 --- a/descriptions/0/api.intercom.io.yaml +++ b/descriptions/0/api.intercom.io.yaml @@ -8249,7 +8249,7 @@ paths: Send the same `from` (a `lead`) and `into` (a `user`) contact IDs you would pass to [Merge a lead and a user](/docs/references/preview/rest-api/api.intercom.io/contacts/mergecontact). - - When `allowed` is `true`, the response contains a `summary`: every attribute change (with the resulting value on the surviving contact) and the number of conversations, notes, and tags that would be reassigned. + - When `allowed` is `true`, the response contains a `summary`: every attribute change (with the resulting value on the surviving contact) and the number of owned conversations, participated conversations, notes, and tags that would be reassigned. - When `allowed` is `false`, the response contains `reasons`. Each reason has a stable `code` you can branch on and a human-readable `message` you can display. {% admonition type="warning" name="Previewing non-duplicate contacts" %} @@ -8298,7 +8298,8 @@ paths: new_into_value: "+1999" total_count: 3 reassignments: - conversations: 14 + owned_conversations: 14 + participated_conversations: 5 tags: 3 notes: 2 blocked: @@ -34860,10 +34861,14 @@ components: type: object description: The number of records that would be reassigned to the surviving contact, by type. Only types with reassignments are included. properties: - conversations: + owned_conversations: type: integer - description: The number of conversations that would be reassigned. + description: The number of conversations the contact owns as the primary user, whose ownership would transfer to the surviving contact. example: 14 + participated_conversations: + type: integer + description: The number of conversations the contact participates in (without owning), whose participation would move to the surviving contact. + example: 5 tags: type: integer description: The number of tags that would be reassigned. From 2a94ef11a39b9d39c1f2c0fbc7c39dfbad6696fb Mon Sep 17 00:00:00 2001 From: Marc <97827744+marcmlc@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:04:17 +0100 Subject: [PATCH 7/7] Report a single distinct conversations count in merge preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow intercom/intercom#536857: the merge preview reports one conversations reassignment count — the distinct number of conversations the merge reassigns, covering both conversations the contact owns and those it participates in (de-duped) — rather than separate owned/participated counts. Co-Authored-By: Claude Opus 4.8 (1M context) --- descriptions/0/api.intercom.io.yaml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/descriptions/0/api.intercom.io.yaml b/descriptions/0/api.intercom.io.yaml index 0214d86..955d690 100644 --- a/descriptions/0/api.intercom.io.yaml +++ b/descriptions/0/api.intercom.io.yaml @@ -8249,7 +8249,7 @@ paths: Send the same `from` (a `lead`) and `into` (a `user`) contact IDs you would pass to [Merge a lead and a user](/docs/references/preview/rest-api/api.intercom.io/contacts/mergecontact). - - When `allowed` is `true`, the response contains a `summary`: every attribute change (with the resulting value on the surviving contact) and the number of owned conversations, participated conversations, notes, and tags that would be reassigned. + - When `allowed` is `true`, the response contains a `summary`: every attribute change (with the resulting value on the surviving contact) and the number of conversations, notes, and tags that would be reassigned. - When `allowed` is `false`, the response contains `reasons`. Each reason has a stable `code` you can branch on and a human-readable `message` you can display. {% admonition type="warning" name="Previewing non-duplicate contacts" %} @@ -8298,8 +8298,7 @@ paths: new_into_value: "+1999" total_count: 3 reassignments: - owned_conversations: 14 - participated_conversations: 5 + conversations: 14 tags: 3 notes: 2 blocked: @@ -34861,14 +34860,10 @@ components: type: object description: The number of records that would be reassigned to the surviving contact, by type. Only types with reassignments are included. properties: - owned_conversations: + conversations: type: integer - description: The number of conversations the contact owns as the primary user, whose ownership would transfer to the surviving contact. + description: The number of distinct conversations that would be reassigned to the surviving contact, counting both conversations the contact owns and conversations it participates in. example: 14 - participated_conversations: - type: integer - description: The number of conversations the contact participates in (without owning), whose participation would move to the surviving contact. - example: 5 tags: type: integer description: The number of tags that would be reassigned.