# Changelog (Preview)

Preview API
The Preview API provides early access to new capabilities we're actively developing. It's suitable for production use, and we're continuing to refine it based on customer feedback. We may make iterative improvements to Preview endpoints, including adding new fields or refining existing ones. When changes are not backward-compatible, we'll communicate them in advance.

For changes that have been updated across all versions, see the [Unversioned changes](/docs/build-an-integration/learn-more/rest-apis/unversioned-changes) page.

## Data connectors: version history

Preview only
These endpoints are available in the Preview API version only. They are not yet included in any stable API version.

You can now read the version history of a data connector and annotate its versions. Versions are returned newest-first, and you can retrieve any version's `definition`, the connector's configuration at that version, without affecting the live connector.

These endpoints require the `Intercom-Version: Preview` header. Reading versions requires the `read_data_connector_versions` OAuth scope, and updating a note requires `read_write_data_connector_versions`.

- [`GET /data_connectors/{id}/versions`](/docs/references/preview/rest-api/api.intercom.io/data-connectors/listdataconnectorversions) — List the versions of a data connector (newest-first, cursor-paginated with `per_page` and `starting_after`).
- [`GET /data_connectors/{id}/versions/{version_id}`](/docs/references/preview/rest-api/api.intercom.io/data-connectors/retrievedataconnectorversion) — Retrieve a single version.
- [`PATCH /data_connectors/{id}/versions/{version_id}`](/docs/references/preview/rest-api/api.intercom.io/data-connectors/updatedataconnectorversion) — Update a version's `note`. Send an empty string to clear it.


Use a version's `entity_version_id` as `version_id`.

## Contact references: new `email` field

Contact references, such as the `contacts` list on conversations and tickets, now include the contact's `email` alongside `type`, `id` and `external_id`. You no longer need a follow-up [`GET /contacts/{id}`](/docs/references/preview/rest-api/api.intercom.io/contacts/showcontact) call to find out which email address a conversation is with. `email` is `null` for a contact with no email on record.

## Auth tokens: `domain` is now read-only

[`POST /auth_tokens`](/docs/references/preview/rest-api/api.intercom.io/auth-tokens/createauthtoken) and [`PATCH /auth_tokens/{id}`](/docs/references/preview/rest-api/api.intercom.io/auth-tokens/updateauthtoken) ignore `domain` if it is sent. Set a credential's domain in the Intercom settings instead. `domain` is still returned on every auth token response, and an update leaves it unchanged. `request_header_key` can't be changed on a credential that has a domain.

## Breaking change: conversation rating `teammate` returns `workflow` for workflow ratings

When a workflow requested a conversation rating and no teammate took part, `conversation_rating.teammate` now returns `type: "workflow"` with the workflow's id. Earlier versions return an `admin` or `team` reference for these ratings, and its id doesn't identify a teammate in your workspace.

This applies to the [Retrieve](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation), [List](/docs/references/preview/rest-api/api.intercom.io/conversations/listconversations) and [Search](/docs/references/preview/rest-api/api.intercom.io/conversations/searchconversations) conversation endpoints, and to the `conversation.rating.added` webhook.

## Dynamic content

You can now manage dynamic content: reusable content with a variation per locale, and optionally per brand, that you can insert into Help Center articles. Update a dynamic content once and every article that uses it shows the new value.

- [`GET /dynamic_contents`](/docs/references/preview/rest-api/api.intercom.io/dynamic-content/listdynamiccontents) lists dynamic content, most recently updated first.
- [`POST /dynamic_contents`](/docs/references/preview/rest-api/api.intercom.io/dynamic-content/createdynamiccontent) creates dynamic content. Set `content_type` to `inline` for a short value such as a phone number, or `block` for one or more blocks of content sent as HTML in `body`.
- [`GET /dynamic_contents/{id}`](/docs/references/preview/rest-api/api.intercom.io/dynamic-content/getdynamiccontent) retrieves a dynamic content.
- [`PUT /dynamic_contents/{id}`](/docs/references/preview/rest-api/api.intercom.io/dynamic-content/updatedynamiccontent) updates its name, default locale or variations. `variations` replaces the full set.
- [`DELETE /dynamic_contents/{id}`](/docs/references/preview/rest-api/api.intercom.io/dynamic-content/deletedynamiccontent) deletes a dynamic content that no article uses.


To make a locale or brand show nothing instead of falling back to the default content, send a variation with `intentionally_empty: true`. Any content sent with it is ignored, and responses report `intentionally_empty` on every variation.

Reading requires the `read_dynamic_content` scope, and creating, updating and deleting require the `read_write_dynamic_content` scope. The workspace must have dynamic content enabled; otherwise these endpoints respond `403`.

The `dynamic_content.created`, `dynamic_content.updated` and `dynamic_content.deleted` [webhook topics](/docs/references/preview/webhooks/webhook-models) notify you of changes.

## Articles: dynamic content in article bodies

The article `body` now keeps dynamic content references when dynamic content is enabled for your workspace. Each reference is an element with the class `intercom-dynamic-content` and a `data-intercom-dynamic-content-key` attribute holding the dynamic content's `key`, wrapping the content shown for the article's locale: a `<div>` for block dynamic content and a `<span>` for inline dynamic content.

```html
<div class="intercom-dynamic-content" data-intercom-dynamic-content-key="refund-policy"><p>You can request a refund within 30 days of purchase.</p></div>
<p>Call us on <span class="intercom-dynamic-content" data-intercom-dynamic-content-key="support-phone-number">+1-555-0100</span>.</p>
```

To keep a reference when you create or update an article, send the element back in `body`. Its inner content is ignored, so edit the dynamic content itself to change what readers see. Removing the element removes the reference.

On earlier API versions, `body` shows inline dynamic content as the literal text `#{{dynamic_content:KEY}}`, which is kept if you send it back. Block dynamic content is left out, so updating an article's `body` from one of those versions removes its block references.

**Affected endpoints:**

- [Retrieve an article](/docs/references/preview/rest-api/api.intercom.io/articles/retrievearticle)
- [Create an article](/docs/references/preview/rest-api/api.intercom.io/articles/createarticle)
- [Update an article](/docs/references/preview/rest-api/api.intercom.io/articles/updatearticle)


## Data connectors: set the transform code block via the API

You can now set a data connector's Python transform code block through [`POST /data_connectors`](/docs/references/preview/rest-api/api.intercom.io/data-connectors/createdataconnector) and [`PATCH /data_connectors/{id}`](/docs/references/preview/rest-api/api.intercom.io/data-connectors/updatedataconnector). The `transform_code_block` field was previously returned only when retrieving a connector. Pass `data_transformation_type: code_block_transformation` with `transform_code_block`:

```json
{
  "data_transformation_type": "code_block_transformation",
  "transform_code_block": {
    "code": "def transform(response):\n    return {\"status\": response[\"order_status\"]}\n\nreturn transform(inputs['data'])"
  }
}
```

For a live connector already in code mode, changing `code` runs the connector's code block test before saving — against its mock response, or its last successful test response. If the test fails, or can't start (for example, the connector has never been tested and has no mock response), the request returns **HTTP 422 Unprocessable Entity** and nothing is saved. Draft connectors are not tested.

## Conversations: AI Topic and Subtopic in `custom_attributes`

Conversation responses include AI Topic and Subtopic classifications inside `custom_attributes`, under the `AI Topic and Subtopic` key. The value is an array of objects, each with `topic_name`, `subtopic_name`, and `id`:

```json
{
  "custom_attributes": {
    "AI Topic and Subtopic": [
      { "topic_name": "Billing", "subtopic_name": "Payment Issues", "id": 42 }
    ]
  }
}
```

This requires the AI add-on. The key is omitted for conversations with no classification. It applies to the [Retrieve](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation), [List](/docs/references/preview/rest-api/api.intercom.io/conversations/listconversations), and [Search](/docs/references/preview/rest-api/api.intercom.io/conversations/searchconversations) conversation endpoints.

## Call transcripts: speaker names

Each turn in a call transcript now carries an optional `speaker_name`, the display name of the teammate or contact who was speaking. Previously a turn only had the generic diarization label — `Teammate 1`, `User` — so you had to work out who said what.

`speaker_name` is omitted when the speaker cannot be resolved or has no name, for example a contact whose record was scrambled by a deletion request. A contact who has been merged into another resolves to the name on the surviving record, and a turn spoken by a teammate who has since been deleted reports `speaker_id` `-1` with the name `Deleted teammate`. Fall back to `speaker_label` when `speaker_name` is absent.

**Affected endpoints:**

- [Get call transcript by call id](/docs/references/preview/rest-api/api.intercom.io/calls/showcalltranscript)
- [List calls with transcripts](/docs/references/preview/rest-api/api.intercom.io/calls/listcallswithtranscripts)


We also corrected the documented response type for [Get call transcript by call id](/docs/references/preview/rest-api/api.intercom.io/calls/showcalltranscript). It was described as a plain-text file attachment; the endpoint has always returned a JSON array of turn objects, and the reference now says so. No behaviour changed.

## New Features

### Fin Voice: `language` on call responses

Fin Voice call responses now return `language`, the language of the call's conversation as a locale code (for example, `fr`). Use it to route the call onward by the caller's language. It is `null` until the call is matched to a conversation, or while no language has been set.

**Affected endpoints:**

- [`GET /fin_voice/collect/{id}`](/docs/references/preview/rest-api/api.intercom.io/calls/collectfinvoicecallbyid)
- [`GET /fin_voice/external_id/{external_id}`](/docs/references/preview/rest-api/api.intercom.io/calls/collectfinvoicecallbyexternalid)
- [`GET /fin_voice/phone_number/{phone_number}`](/docs/references/preview/rest-api/api.intercom.io/calls/collectfinvoicecallbyphonenumber)
- [`GET /fin_voice/conversation/{conversation_id}`](/docs/references/preview/rest-api/api.intercom.io/calls/collectfinvoicecallsbyconversationid)


### Scorecard entries now include who submitted and who last revised the review

Each entry in the `scorecards` array returned with `include_scorecards=true` now tells you who acted on the review, alongside `reviewer` (who the review is assigned to):

- `submitted_by` — who last submitted the review, as `{ "type": "ai" }` when Fin submitted it or `{ "type": "admin", "admin_id": "..." }` when a teammate did. This can be a different teammate from the `reviewer`. Null while the review is unreviewed or when the scorecard has no review.
- `revised_by` — who last changed the review after it was submitted, in the same shape. Null while the review is unreviewed, when it has not been changed since it was submitted, or when the scorecard has no review.


```json
{
  "scorecard_id": "67890",
  "name": "Standard QA Review",
  "reviewer": { "type": "admin", "admin_id": "884120017" },
  "review_status": "submitted",
  "submitted_by": { "type": "admin", "admin_id": "884120017" },
  "revised_by": null
}
```

The fields are returned by [`GET /conversations`](/docs/references/preview/rest-api/api.intercom.io/conversations/listconversations), [`GET /conversations/{id}`](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation) and [`POST /conversations/search`](/docs/references/preview/rest-api/api.intercom.io/conversations/searchconversations). Set `Intercom-Version: Preview`.

### Fin platform handoffs: new `event_details` on the conversation part

A `standalone_platform_handoff` conversation part now returns `event_details.handoff`, describing the handoff Fin attempted to an external platform, so you can tell what kind of handoff happened and whether it worked without reading the part's body.

**New field:**

- `event_details.handoff.type` (string) — the handoff type: `zendesk_ticket`, `zendesk_agent`, `salesforce_case`, `salesforce_in_app_messaging`, `salesforce_team`, `external_agent` or `external_ticket`. Older parts can also return `salesforce_chat`.
- `event_details.handoff.success` (boolean or `null`) — `true` when the handoff completed and `false` when it failed. `null` means no outcome was recorded on this part, for example a Salesforce in-app messaging handoff that was only initiated.
- `event_details.handoff.data` (object) — always present. For a successful `salesforce_case` handoff it holds the created Case's `case_id` and `case_number`; otherwise it is an empty object.


```json
"event_details": {
  "handoff": {
    "type": "salesforce_case",
    "success": true,
    "data": { "case_id": "500XX0000001", "case_number": "00001234" }
  }
}
```

The field is available in the Preview API version only (set `Intercom-Version: Preview`).

### Conversations: list team handling segments

You can now see which team a conversation's active handling time belongs to. [`GET /conversations/{id}/team_handling_segments`](/docs/references/preview/rest-api/api.intercom.io/conversations/listteamhandlingsegments) returns the periods teammates actively handled the conversation, each tagged with the team assigned to it at the time.

- Each period covers a single team assignment and never continues across a team change. Moving a conversation to another team usually unassigns the teammate as well, so their period ends with an `end_reason` of `assignment`, and the next period starts, with a `start_reason` of `null`, when a teammate picks the conversation up again. You'll only see `team_change` when the team changes and the same teammate keeps working without a break.
- `end_reason` is one of `switch`, `assignment`, `reassign`, `snooze`, `close`, `away`, `auto_away`, `session_drop` or `team_change`.
- `team` is `null` for a period when no team was assigned.
- `ended_at` and `end_reason` are `null` while a period is still open.
- The response doesn't say which teammate did the work.


Requires the `Intercom-Version: Preview` header and the `read_conversations` scope. Responds `404` if the conversation does not exist, and `403` if the teammate cannot access it.

### Contacts: read email_verified back on the contact object

The contact object response now includes `email_verified` field, so you can read back the value set when you [create](/docs/references/preview/rest-api/api.intercom.io/contacts/createcontact) or [update](/docs/references/preview/rest-api/api.intercom.io/contacts/updatecontact) a contact. Setting `email_verified` field has been supported since API version 2.16.

`email_verified` is `true` only when the contact's current email address was marked verified through the Contacts API and has not changed since; it becomes `false` again if the contact's email changes, until the new address is verified. A lead with a verified email is reused when Intercom later matches an inbound email, or an outbound conversation or ticket, to that address, instead of creating a new lead.

**Affected endpoints:**

- [`GET /contacts/{id}`](/docs/references/preview/rest-api/api.intercom.io/contacts/showcontact)
- [`GET /contacts/find_by_external_id/{external_id}`](/docs/references/preview/rest-api/api.intercom.io/contacts/showcontactbyexternalid)
- [`GET /contacts`](/docs/references/preview/rest-api/api.intercom.io/contacts/listcontacts)
- [`POST /contacts/search`](/docs/references/preview/rest-api/api.intercom.io/contacts/searchcontacts)
- [`POST /contacts`](/docs/references/preview/rest-api/api.intercom.io/contacts/createcontact)
- [`PUT /contacts/{id}`](/docs/references/preview/rest-api/api.intercom.io/contacts/updatecontact)
- [`POST /contacts/merge`](/docs/references/preview/rest-api/api.intercom.io/contacts/mergecontact)
- [`GET /companies/{id}/contacts`](/docs/references/preview/rest-api/api.intercom.io/companies/listattachedcontacts)
- [`POST /visitors/convert`](/docs/references/preview/rest-api/api.intercom.io/visitors/convertvisitor)


### Contacts: `whatsapp_number` on the contact object

The contact object now returns `whatsapp_number`, the WhatsApp number stored for the contact (for example, `+34666666666`), or `null` when the contact has none. It is read-only: sending it when creating or updating a contact has no effect.

**Affected endpoints:**

- [`GET /contacts/{id}`](/docs/references/preview/rest-api/api.intercom.io/contacts/showcontact)
- [`GET /contacts/find_by_external_id/{external_id}`](/docs/references/preview/rest-api/api.intercom.io/contacts/showcontactbyexternalid)
- [`GET /contacts`](/docs/references/preview/rest-api/api.intercom.io/contacts/listcontacts)
- [`POST /contacts/search`](/docs/references/preview/rest-api/api.intercom.io/contacts/searchcontacts)
- [`POST /contacts`](/docs/references/preview/rest-api/api.intercom.io/contacts/createcontact)
- [`PUT /contacts/{id}`](/docs/references/preview/rest-api/api.intercom.io/contacts/updatecontact)
- [`POST /contacts/merge`](/docs/references/preview/rest-api/api.intercom.io/contacts/mergecontact)
- [`GET /companies/{id}/contacts`](/docs/references/preview/rest-api/api.intercom.io/companies/listattachedcontacts)
- [`POST /visitors/convert`](/docs/references/preview/rest-api/api.intercom.io/visitors/convertvisitor)
- Webhook topics whose payload includes a contact object


### Conversations: HTML tables in replies and notes are preserved

A reply or note whose `body` contains `<table>` markup is stored as a real table, so a later `GET` returns table markup instead of a single paragraph of concatenated cell text. Previously the structure was discarded and only the cell text survived.

This is enabled per workspace and is still rolling out. Until your workspace has access, a `<table>` in a reply `body` continues to be flattened to plain text, as it is today.

The request contract is unchanged — `body` is still an HTML string and there is no new parameter. Conversation parts created before this change are unaffected, and a `body` with no `<table>` behaves exactly as it did.

A table-bearing `body` is checked before conversion: at most 200 rows and 50 columns per table, 20 tables and 10,000 cells per message, and no nested tables. Exceeding any limit returns `400 Bad Request` naming the limit, rather than truncating. Note that when a `body` contains a table, the whole body takes a different conversion path, so other markup in it can come back differently — see [Allowed HTML for conversation replies](/docs/guides/conversations/supported-html) for the supported tags, the limits, and the known differences.

**Affected endpoints:**

- [`POST /conversations/{id}/reply`](/docs/references/preview/rest-api/api.intercom.io/conversations/replyconversation) — both `message_type: comment` and `message_type: note`
- [`POST /tickets/{id}/reply`](/docs/references/preview/rest-api/api.intercom.io/tickets/replyticket) — both `message_type: comment` and `message_type: note`


On a ticket, `message_type: comment` is rejected for tracker tickets, and for back-office tickets that are not shared with the customer. Those accept `message_type: note` only. This is existing ticket-reply behaviour and is not specific to tables.

## Breaking Changes

### QA review attributes removed from conversation `custom_attributes`

`custom_attributes` on conversation responses and webhook payloads no longer includes the attributes written by QA monitors and scorecards: the system-defined `Review status`, `Reviewer`, `Has QA Notes` and `QA Notes Count`, plus any conversation attribute a QA monitor or scorecard created.

On the conversation endpoints, read QA results as structured data with `include_monitors=true` and `include_scorecards=true`. Webhooks take no such parameter, so a conversation webhook payload no longer carries QA data at all — retrieve the conversation with those parameters when you need it.

This applies to [`GET /conversations`](/docs/references/preview/rest-api/api.intercom.io/conversations/listconversations), [`GET /conversations/{id}`](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation), [`POST /conversations/search`](/docs/references/preview/rest-api/api.intercom.io/conversations/searchconversations), and conversation webhook payloads. Workspaces on API version 2.16 or earlier that were explicitly opted in to see these attributes continue to see them.

### Opaque ids removed from scorecard entries

Conversation responses with `include_scorecards=true` no longer include these fields:

- `scorecard_version_id` on each entry in `scorecards`
- `reason_ids` on each `result` in an entry's `evaluators`


`scorecard_id` still identifies the scorecard. `result.reasons` gives the reason names. `evaluator_id` and `result.value` are unchanged.

This applies to [`GET /conversations`](/docs/references/preview/rest-api/api.intercom.io/conversations/listconversations), [`GET /conversations/{id}`](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation) and [`POST /conversations/search`](/docs/references/preview/rest-api/api.intercom.io/conversations/searchconversations). API version 2.16 and earlier still return both fields.

## Roles: read a workspace's roles

The teammates endpoint returns the role a teammate holds, but only the roles currently assigned, and only their id and name. You can now list every role in your workspace and look one up by id, each with its description.

- [`GET /roles`](/docs/references/preview/rest-api/api.intercom.io/roles/listroles) — List every role in the workspace.
- [`GET /roles/{id}`](/docs/references/preview/rest-api/api.intercom.io/roles/retrieverole) — Retrieve a single role. Responds `404` if no role with that id exists in your workspace.


Both endpoints require the `Intercom-Version: Preview` header and an access token with the `read_admins` scope. Each role returns its `id`, `name` and `description`; `description` is null when the role does not have one. Roles are read-only over the API — they are created and edited in your workspace settings.

## Auth tokens: create and update

You can now store a third-party credential in your workspace over the API, and update one you already have. Together with the existing read endpoints, you can set up a data connector and the credential it authenticates with without leaving your integration.

- [`POST /auth_tokens`](/docs/references/preview/rest-api/api.intercom.io/auth-tokens/createauthtoken) — Store a new credential. `name`, `token_value` and `request_header_key` are required; `token_prefix`, `description` and `domain` are optional.
- [`PATCH /auth_tokens/{id}`](/docs/references/preview/rest-api/api.intercom.io/auth-tokens/updateauthtoken) — Update a credential's `name`, `request_header_key`, `token_prefix`, `description` or `domain`. A field you omit keeps its current value.


Both endpoints need an access token with the `write_auth_tokens` scope, and that token must be associated with a teammate — an app-scoped token gets a `403`. Only credentials with a `token_type` of `text` can be created or updated; any other type returns `400` from the update endpoint.

The credential value you send is stored encrypted and is never returned by any endpoint, including the one that created it. It also cannot be changed through the update endpoint. A credential the list endpoint does not return cannot be updated either, and responds with `404`.

## Auth tokens: create an OAuth credential

[`POST /auth_tokens`](/docs/references/preview/rest-api/api.intercom.io/auth-tokens/createauthtoken) now accepts `token_type: oauth`. Give it an `oauth_configuration` with `token_url`, `client_id` and `client_secret` (`scope`, `audience` and `request_format` are optional), and Intercom exchanges the client credentials for an access token using the OAuth client-credentials grant, then keeps it refreshed automatically. Requests sent with the credential use the `Authorization` header with a `Bearer` prefix.

`token_value`, `request_header_key` and `token_prefix` must be omitted for an `oauth` credential — they only apply to `text` credentials, which remain the default `token_type`. The client secret you send is never returned by any endpoint. If Intercom can't obtain an access token from `token_url` with the given client credentials, creation fails with `400`.

## New Features

### Scorecard evaluators now include readable names, labels and reasons

Each evaluator in a scorecard entry's `evaluators` array previously exposed only ids. It now also carries the human-readable name and result labels, so you can display a scorecard without looking up every id yourself:

- `name` — the evaluator's (criterion's) name. Null if it could not be resolved.
- `result.value_label` — the label of the rating option matching `result.value` (for example `Needs improvement`). Omitted when the label could not be resolved.
- `result.reasons` — the structured reasons chosen for the result, each with a `name`. Omitted when none were chosen.


```json
{
  "evaluator_id": "54321",
  "name": "Tone of voice",
  "result": {
    "value": "pass",
    "value_label": "Pass",
    "source": "ai",
    "reasoning": "The agent acknowledged the issue and resolved it within the same response.",
    "reasons": [
      { "name": "Clear and concise" },
      { "name": "Empathetic tone" }
    ]
  }
}
```

The fields are returned by [`GET /conversations`](/docs/references/preview/rest-api/api.intercom.io/conversations/listconversations), [`GET /conversations/{id}`](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation) and [`POST /conversations/search`](/docs/references/preview/rest-api/api.intercom.io/conversations/searchconversations). Set `Intercom-Version: Preview`.

### Scorecard entries now include review notes

Each entry in the `scorecards` array returned with `include_scorecards=true` now carries the notes teammates left on that scorecard's QA review, so you can read the reviewer's commentary next to the score it explains:

- `notes` — an array holding each note on the linked review, oldest first. Empty when the scorecard has no linked review.
- Each note has an `id`, `created_at` and `updated_at` timestamps, an `author` and a `body`. `author` is `{ "type": "admin", "admin_id": "..." }` when a teammate wrote the note, `{ "type": "ai" }` when Fin did, and null when the teammate who wrote it has since been deleted. The body is the plain text the teammate typed; it is not rendered as HTML.


```json
{
  "scorecard_id": "67890",
  "name": "Standard QA Review",
  "reviewer": { "type": "admin", "admin_id": "884120017" },
  "review_status": "submitted",
  "notes": [
    {
      "id": "9001",
      "created_at": 1734537900,
      "updated_at": 1734537900,
      "author": { "type": "admin", "admin_id": "884120017" },
      "body": "Great empathy in the second reply."
    }
  ]
}
```

The field is returned by [`GET /conversations`](/docs/references/preview/rest-api/api.intercom.io/conversations/listconversations), [`GET /conversations/{id}`](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation) and [`POST /conversations/search`](/docs/references/preview/rest-api/api.intercom.io/conversations/searchconversations). Set `Intercom-Version: Preview`.

### Scorecard entries now include the reviewer and review status

Each entry in the `scorecards` array returned with `include_scorecards=true` now tells you who the review is assigned to and where that review stands, alongside the existing `reviewed_teammate` (the teammate or AI agent whose handling was scored):

- `reviewer` — who the review is assigned to, as `{ "type": "ai" }` when it is assigned to Fin or `{ "type": "admin", "admin_id": "..." }` when it is assigned to a teammate. Null when the scorecard has no review yet or the review is unassigned.
- `review_status` — the status shown in the QA reviews list: `unreviewed`, `submitted`, `passed_review`, `needs_fix`, `wont_fix` or `fix_complete`. Null when the scorecard has no review.


```json
{
  "scorecard_id": "67890",
  "name": "Standard QA Review",
  "passed": true,
  "score": 0.85,
  "reviewed_teammate": { "type": "admin", "admin_id": "991267715" },
  "reviewer": { "type": "admin", "admin_id": "884120017" },
  "review_status": "submitted"
}
```

The fields are returned by [`GET /conversations`](/docs/references/preview/rest-api/api.intercom.io/conversations/listconversations), [`GET /conversations/{id}`](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation) and [`POST /conversations/search`](/docs/references/preview/rest-api/api.intercom.io/conversations/searchconversations). Set `Intercom-Version: Preview`.

### Failed OTP email verifications now tell you why

An `ai_agent_event` conversation part with the event type `otp_email_verification_failed` now carries a `reason` naming why the one-time passcode email could not verify the contact:

```json
{
  "part_type": "ai_agent_event",
  "event_details": {
    "event": {
      "type": "otp_email_verification_failed",
      "value": "OTP email verification failed",
      "data": {
        "otp_email_verification_failed": {
          "reason": "no_email"
        }
      }
    }
  }
}
```

The reason you can receive today is `no_email`: the workspace requires email verification before Fin runs an authenticated action, and the contact has no email address on record.

Treat `reason` as a free-form string. More reasons may follow, so fall back to the event's `value` for a reason you do not recognise. A failure recorded without a reason returns an empty `data` object.

This applies to the conversation and conversation part endpoints and to conversation webhook topics. Earlier versions continue to return an empty `data` object for this event.

## Auth tokens API

You can now read the third-party credentials stored in your workspace. A data connector authenticates with a stored credential, and these endpoints let you find the id of the one you want it to use.

- [`GET /auth_tokens`](/docs/references/preview/rest-api/api.intercom.io/auth-tokens/listauthtokens) — List the workspace's stored credentials. Paginated with `per_page` (default 20, maximum 50) and `starting_after`.
- [`GET /auth_tokens/{id}`](/docs/references/preview/rest-api/api.intercom.io/auth-tokens/retrieveauthtoken) — Retrieve one credential by id.


Both endpoints need an access token with the `read_auth_tokens` scope, and both are read-only. They return metadata only — the credential value, and its refresh and OAuth configuration, are never included. The list is not exhaustive: credentials a teammate created are listed, as are those created by some Intercom integrations, but credentials installed by an app package, those created by most other integrations, and those minted for an individual teammate's MCP access are not. A credential missing from the list also returns `404` from the retrieve endpoint.

## Contacts: user-to-user merge support

[`POST /contacts/merge`](/docs/references/preview/rest-api/api.intercom.io/contacts/mergecontact) now accepts a source contact (`from`) with a `role` of `user` as well as `lead`. The destination (`into`) must still be a `user`.

When you merge a user into another user, the source user's attribute values (standard and custom) are discarded and are not copied onto the target. The target user keeps its own attribute values. Records associated with the source, such as conversations, notes, and tags, are still reassigned to the target.

## Breaking change: unrecognised conversation part types report as `unknown`

A conversation part whose type this API version does not recognise now returns `part_type: "unknown"`. Earlier versions return `comment` for those parts.

This affects a small number of part types that were retired from the product. If you branch on `part_type`, treat `unknown` as a part you should skip rather than as a message — previously such a part was indistinguishable from a real `comment`.

`unknown` is a value you may receive but cannot filter on: it is rejected by `include_part_types` and `exclude_part_types` with a **HTTP 422 Unprocessable Entity**, because the set of types behind it is open-ended. To leave those parts out, name the types you do want with `include_part_types`.

Ticket parts are unaffected and continue to report `comment`.

## Granted companies API

You can now manage which companies a contact has been granted access to. A granted company lets the contact read that company's tickets in the customer portal without being a member of it — useful when a consultant or agency contact needs visibility of the companies they act for.

Grants are separate from company membership: granting a company does not attach the contact to it, and [`GET /contacts/{id}/companies`](/docs/references/preview/rest-api/api.intercom.io/contacts/listcompaniesforacontact) does not include grants.

- [`GET /contacts/{contact_id}/granted_companies`](/docs/references/preview/rest-api/api.intercom.io/contacts/listgrantedcompaniesforacontact) — List a contact's granted companies. Paginated with `page` and `per_page` (default 50, maximum 150), sorted by company id, and `order` accepts `asc` or `desc`.
- [`PUT /contacts/{contact_id}/granted_companies`](/docs/references/preview/rest-api/api.intercom.io/contacts/replacegrantedcompaniesforacontact) — Replace the contact's full granted set.
- [`DELETE /contacts/{contact_id}/granted_companies/{company_id}`](/docs/references/preview/rest-api/api.intercom.io/contacts/removegrantedcompanyfromacontact) — Remove one company from the granted set, returning `204` with no body.


Companies are identified by the `company_id` you set on them, so what `GET` returns is what you send back to `PUT`.

`PUT` is a replacement, not an append. The `company_ids` you send become the contact's entire granted set and any id you leave out is dropped, so to add a company you send the existing ids plus the new one, and to drop every grant you send `company_ids: []`. Omitting `company_ids` altogether is rejected with `400` `parameter_not_found` rather than treated as an empty list. A contact can be granted at most 100 companies.

`DELETE` is not idempotent. Removing a company the contact is not currently granted returns `404` `company_not_found`, so repeating a successful call returns `404` rather than `204`.

Listing works with the `read_users_companies` OAuth scope; replacing and removing require the `write_users_companies` OAuth scope. All three endpoints need the `Intercom-Version: Preview` header.

Requires granted companies to be enabled
`PUT` and `DELETE` return `404` with the code `granted_companies_not_enabled` unless granted companies are enabled for the workspace, and `GET` returns an empty list. Contact Intercom Support to enable it.

## Retrieve the human-in-the-loop events for a conversation

When Fin is unsure of an answer it can consult a teammate before replying, proposing a draft that the teammate approves, steers with feedback, or takes over. [`GET /conversations/{id}/hitl_events`](/docs/references/preview/rest-api/api.intercom.io/conversations/listhitlevents) returns those consultations as a list of events, oldest first, across every consultation on the conversation.

```json
{
  "type": "list",
  "data": [
    {
      "hitl_cycle_id": "3f2a91c4-8e77-4d61-9a2b-0c5d7e1f4a80",
      "event_type": "fin_teammate_input_received",
      "conversation_part_id": null,
      "rule_id": "88",
      "rule_name": "Refunds over $100",
      "created_at": 1767225701,
      "data": {
        "round_index": 1,
        "teammate_input": "Tell them 5 business days.",
        "reviewer_admin_id": "4001",
        "regeneration_outcome": "answer"
      }
    }
  ]
}
```

Which fields appear in `data` depends on `event_type`:

| `event_type` | `data` fields |
|  --- | --- |
| `fin_consultation_started` | `channel`, `rule_threshold_percent`, `confidence_percent`, `sharp_ask`, `ask_rationale`, `issue_summary`, `customer_question` |
| `fin_draft_proposed` | `draft` |
| `fin_teammate_input_received` | `round_index`, `teammate_input`, `reviewer_admin_id`, `regeneration_outcome` |
| `fin_draft_revised` | `round_index`, `revised_draft`, `regeneration_outcome` |
| `fin_consultation_finished` | `outcome`, `approved_draft_type`, `final_draft`, `sent_text`, `reviewer_admin_id`, `duration_seconds`, `rounds_count`, `payload_truncated` |


Every consultation produces a `fin_consultation_started` and a `fin_consultation_finished` event. `fin_draft_proposed` appears when Fin proposed a draft, `fin_teammate_input_received` once per round of teammate feedback, and `fin_draft_revised` once per round where Fin produced a revised draft in response. A consultation ends with an `outcome` of `approved`, `taken_over`, `timed_out_sent`, `timed_out_escalated` or `superseded`.

`hitl_cycle_id` identifies the consultation, so it is the same on every event of one consultation rather than unique per event. Group events by it to reconstruct a single consultation. A conversation can hold several — one that replaces an outstanding consultation finishes as `superseded`.

A few things to know when reading the response:

- Events appear only once a consultation has ended, so one still awaiting a teammate is absent.
- `conversation_part_id` is `null` where an event has no corresponding conversation part. Teammate feedback given through the inbox creates no part, a revised draft has no part of its own, and a consultation that escalated or was superseded sent no reply.
- `rule_id`, `rule_name` and `rule_threshold_percent` are recorded when the consultation begins and not updated afterwards, so a rule renamed or retuned since will not match its current settings. They are `null` for consultations no rule triggered.
- `reviewer_admin_id` is the teammate involved in that event: on `fin_teammate_input_received` whoever gave the feedback, on `fin_consultation_finished` whoever approved the draft or took over. On a consultation with several rounds these can be different teammates.
- `rounds_count` counts every round of feedback the consultation had, while only the ten most recent are returned as events. A heavily steered consultation therefore returns fewer `fin_teammate_input_received` events than `rounds_count` reports.
- Redacting a conversation removes this data. Depending on how it was redacted, the conversation returns no events, or events whose text fields are all `null`.


Requires the `read_conversations` scope, and the conversation must be one the caller can access.

## Set a ticket's `priority` when updating it

You can now set a ticket's priority level with [`PUT /tickets/{id}`](/docs/references/preview/rest-api/api.intercom.io/tickets/updateticket). Pass `priority` with one of `none`, `low`, `medium`, `high`, or `urgent`:

```json
{
  "priority": "high"
}
```

Set `none` to clear an existing priority. Sending the level a ticket already has succeeds and changes nothing, so retries are safe.

The change is attributed to Operator by default. Pass the existing `admin_id` field to attribute it to a teammate instead — that teammate needs Inbox access, or the request returns **HTTP 403 Forbidden**. An unrecognised level returns **HTTP 422 Unprocessable Entity** and no part of the update is applied.

The ticket response does not include `priority`; this change covers the write only.

## Data connectors expose their transform code block

[Retrieving a data connector](/docs/references/preview/rest-api/api.intercom.io/data-connectors/retrievedataconnector) now includes `transform_code_block` when the connector's `data_transformation_type` is `code_block_transformation`. It contains the `code` and `language` of the code block used to transform the connector's response. The field is omitted for connectors that don't use a code block transformation.

```json
{
  "data_transformation_type": "code_block_transformation",
  "transform_code_block": {
    "code": "def transform(response):\n    return {\"status\": response[\"order_status\"]}\n\nreturn transform(inputs['data'])",
    "language": "python"
  }
}
```

## Conversation and ticket parts report the teammate and team they assigned

Conversation parts and ticket parts now carry `admin_assignee_id` and `team_assignee_id`, the teammate and the team a part assigned the conversation or ticket to. They appear on [conversations](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation) and [tickets](/docs/references/preview/rest-api/api.intercom.io/tickets/getticket), and on the webhook payloads built from the same parts. They are named to match the `admin_assignee_id` and `team_assignee_id` already returned on conversations and tickets.

A part records a single assignee, so when a part assigns a team and a teammate at the same time, `assigned_to` reports the teammate only. This happens whenever a team's Round Robin distribution resolves the teammate inline, which meant the team a conversation was routed to was not reported at all. The new fields report each assignee separately, so team routing is visible regardless of the team's distribution method:

```json
{
  "type": "conversation_part",
  "part_type": "assignment",
  "assigned_to": { "type": "admin", "id": "991267715" },
  "admin_assignee_id": 991267715,
  "team_assignee_id": 5017691
}
```

Both fields are populated for parts created before this change as well as new ones, so no backfill is needed to see historical team routing.

How each field behaves, using teammate `991267715`, team `5017691` and bot `278`:

| What the part did | `assigned_to` | `admin_assignee_id` | `team_assignee_id` |
|  --- | --- | --- | --- |
| Assigned a team, resolving a teammate inline (Round Robin) | `{"type": "admin", "id": "991267715"}` | `991267715` | `5017691` |
| Assigned a teammate | `{"type": "admin", "id": "991267715"}` | `991267715` | `null` |
| Assigned a team | `{"type": "team", "id": "5017691"}` | `null` | `5017691` |
| Assigned a bot | `{"type": "bot", "id": "278"}` | `278` | `null` |
| Removed the team | `{"type": "nobody_admin", "id": null}` | `null` | `0` |
| Removed the teammate | `{"type": "nobody_admin", "id": null}` | `0` | `null` |
| Unassigned without naming which kind | `{"type": "nobody_admin", "id": null}` | `null` | `null` |
| Changed no assignment, for example a comment | `null` | `null` | `null` |


A bot appears in `admin_assignee_id`, because assignment treats anything that is not a team as a teammate. On endpoints that omit empty fields, both are absent rather than `null` — the same as the neighbouring `assigned_to`.

## New Features

### Internal articles in AI Agent content sources

The `ai_agent.content_sources` list on a conversation now includes the internal articles Fin used, reported with `content_type` of `internal_article` alongside the article's title, locale, and a link your teammates can open.

Earlier versions leave internal articles out of the list, so a conversation answered from one returns fewer sources than Fin actually used.

### Conversations: new `drop_reason` values for spam complaints and full mailboxes

`drop_reason` can now report `user_marked_as_spam` and `user_mailbox_full_suppressed`, two new reasons a recipient's address was suppressed:

- `user_marked_as_spam` — the address was suppressed after a spam complaint, from an email provider's feedback loop or an in-app report.
- `user_mailbox_full_suppressed` — the address was suppressed because its mailbox is full.


`user_suppressed` is still returned, but no longer implies either cause: it now covers only addresses suppressed manually (for example, by a support teammate) and drop reasons recorded before this change. This applies to the Preview API version only.

### Filter conversation parts by type

[Retrieving a conversation](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation) accepts two new optional parameters. `include_part_types` returns only the part types you name; `exclude_part_types` returns everything except them. Both take a comma-separated list of up to 50 part types, and a request can use one or the other, not both.

The filter is applied before the limit on how many parts are returned, so you get the newest parts that match rather than whichever parts fit inside the limit. A conversation with thousands of assignment and workflow parts will still return your most recent comments.

```
GET /conversations/123?include_part_types=comment,note
```

The values you pass are the same names this version returns in each part's `part_type` field, so you can read a conversation once and filter by what you see. An unrecognised name returns a `422` rather than being ignored — a typo fails loudly instead of quietly returning an unfiltered list.

Two names are worth knowing about before you filter:

- `unknown` cannot be filtered on. A part type this version does not recognise is returned as `unknown`, and passing that name returns a `422` — the set of types behind it is open-ended. Use `include_part_types` to name the types you do want instead.
- The part type that versions below 2.6 return as `note_and_reopen` is named `note_and_unsnooze` here. If you are moving a pinned integration onto Preview, filtering on the older name returns a `422`.


Available on Preview only. Other versions return a `422`.

### List a workspace's deleted contacts

You can now fetch a paginated list of the contacts in your workspace that are no longer active with [`GET /contacts/deleted`](/docs/references/preview/rest-api/api.intercom.io/contacts/listdeletedcontacts). This helps you recover contacts whose `id`, `email`, or `external_id` you no longer have so you can permanently delete them.

Each entry includes a `status` explaining why the contact is inactive — one of `archived`, `blocked`, `merged`, `pending_deletion`, or `permanently_deleted` — along with its `id`, `role`, and an `updated_at` UNIX timestamp. `merged_into_id` is included only when `status` is `merged`. For `permanently_deleted` contacts, `external_id` and `email` are returned as `null`.

Results are paginated with `page` and `per_page` (default `10`, maximum `150`). A non-positive `page` or `per_page` returns **HTTP 400 Bad Request**.

### Assign a closed conversation without reopening it

Assigning a closed conversation reopens it. You can now keep it closed by passing `skip_reopen` on an assignment to [`POST /conversations/{id}/parts`](/docs/references/preview/rest-api/api.intercom.io/conversations/manageconversation):

```json
{
  "message_type": "assignment",
  "type": "admin",
  "admin_id": "991267708",
  "assignee_id": "4324241",
  "skip_reopen": true
}
```

The conversation keeps its original close timestamp, no reopen event is recorded, and the assignment does not count as a new closure in reporting. This lets you reassign a batch of closed conversations — when a teammate leaves, or an inbox is retired — without changing your closure metrics.

`skip_reopen` is optional and defaults to `false`, so assignment behaves as before unless you set it. Open and snoozed conversations are unaffected.

### Remove an admin

[`DELETE /admins/remove`](/docs/references/preview/rest-api/api.intercom.io/admins/removeadmin) removes a teammate from your workspace, reassigning their auto-messages, ownership, article authorship, and conversation replies to the teammates you specify. All four reassignment targets are required — pass `0` for `reassign_conversations_admin_id` to leave their conversations unassigned. Requires the `remove_admins` scope.

### Delete a note

You can now delete a single contact or company note with [`DELETE /notes/{id}`](/docs/references/preview/rest-api/api.intercom.io/notes/deletenote). Notes of other kinds cannot be deleted through the API. Requires the `write_users_companies` OAuth scope.

### Update a team's members

You can now replace the set of teammates on a team with [`PUT /teams/{id}`](/docs/references/preview/rest-api/api.intercom.io/teams/updateteammembers). Send the full list of teammates the team should end up with — anyone currently on the team and missing from `admin_ids` is removed:

```json
{
  "admin_ids": [493881, 493882]
}
```

Sending an unchanged list makes no changes, so you can call the endpoint on a schedule to keep a team in step with an external rota or workforce management system.

On a team that balances assignment across its members, pass `admin_priority_level` to set which teammates take work first:

```json
{
  "admin_ids": [493881, 493882],
  "admin_priority_level": {
    "primary_admin_ids": [493881],
    "secondary_admin_ids": [493882]
  }
}
```

`admin_priority_level` is optional and names only the teammates whose level you are setting. A teammate you leave out keeps the level they already have, and one joining the team without a level named becomes a primary member. Only a team that balances assignment can have secondary members.

Membership is the only part of a team this endpoint writes. You can include a team's other fields as long as they are unchanged; changing one returns **HTTP 400 Bad Request**. `admin_ids` has to name at least one teammate, so the endpoint cannot empty a team, and a team that already has no members cannot be sent back as it reads. A token acting on behalf of a teammate carries that teammate's permissions: they must be able to manage teams, and any team or teammate their access is restricted from stays out of reach. Where the endpoint is not available for a workspace, the request returns **HTTP 403 Forbidden** with the code `api_plan_restricted`.

If two requests change the same team at once, one returns **HTTP 409 Conflict**. The message says whether the team was left unchanged — in which case send the same request again — or the change was partly applied, in which case read the team before retrying.

### Conversation and ticket parts: new `translations` field for translated admin replies

Admin reply conversation parts and ticket parts can now return a `translations` field carrying the reply text in multiple locales. It backs the new `conversation.admin.replied.translated` and `ticket.admin.replied.translated` webhook topics.

**New field:**

- `translations` (object, nullable) — a map keyed by locale code, with values in the same HTML format as `body`. The special `original` key holds the source locale code of the reply; every other key is a locale code whose value is the reply text translated into that locale. Example: `{ "original": "en", "en": "<p>Hello</p>", "es": "<p>¡Hola!</p>" }`.


`translations` does not modify the existing `body` field — `body` carries the reply as authored. The field is available in the Preview API version only (set `Intercom-Version: Preview`).

Both webhook topics only fire when the reply's `body` has no bare text at the top level — a plain-text reply, such as one sent through the public REST API without HTML markup, is suppressed. See the [webhook topics reference](/docs/references/preview/webhooks/webhook-models) for details.

## New incident webhook topics

You can now receive real-time notifications when Intercom detects a spike in conversation volume, using the new `incident.created` and `incident.updated` webhook topics. The payload carries the incident's generated title and description, the conversation `topics` it covers, and `excess_topic_conversations` — the number of conversations above the expected volume for those topics.

`incident.updated` fires when a new spike of conversations is grouped into an existing incident, which can also regenerate its title and description — treat the payload as a full replacement rather than diffing it. There is no `incident.closed` topic: incidents close when detection stops seeing activity, and no event is sent when they do.

Both topics require the **Export reporting data** permission. See the [webhook topics reference](/docs/references/preview/webhooks/webhook-models) for payload details, and [Set up Webhooks](/docs/webhooks/setting-up-webhooks) to configure a subscription.

## Set a conversation's `priority` when updating it

You can now set a conversation's priority level with [`PUT /conversations/{id}`](/docs/references/preview/rest-api/api.intercom.io/conversations/updateconversation). Pass `priority` with one of `none`, `low`, `medium`, `high`, or `urgent`:

```json
{
  "priority": "high"
}
```

Set `none` to clear an existing priority. Sending the level a conversation already has succeeds and changes nothing, so retries are safe.

The change is attributed to Operator by default. Pass `admin_id` to attribute it to a teammate instead — that teammate needs Inbox access, or the request returns **HTTP 403 Forbidden**.

## List contact activities

You can now [list contact activities](/docs/references/preview/rest-api/api.intercom.io/contacts/listcontactactivities) with `GET /contacts/activities`. The endpoint returns a paginated feed of contact lifecycle events on a workspace level — such as creation, role update, merge, and deletion — ordered oldest first. Results use cursor-based pagination via `starting_after`.

## Conversations: `call_summary` parts now identify the call they describe

`call_summary` conversation parts now return `event_details` containing the id of the call the summary describes. A conversation can contain several calls, and therefore several `call_summary` parts side by side; previously nothing in the response indicated which call each summary belonged to.

**New field:**

- `event_details.call_id` (string) — the id of the call the summary describes. It matches the `id` returned by the Calls API and by the `call.started`, `call.ended`, `call.recording_available` and `call.transcription_available` webhook topics, so it can be used as a direct join key.


Call summaries are written asynchronously after the call transcript has been processed, so the part's `body` can still be empty when you first read the conversation. `event_details.call_id` is present from the moment the part is created, regardless of whether the summary text has been generated yet.

**Affected endpoints:**

- [Retrieve a conversation](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation)


Conversation parts are only returned by endpoints that return a full conversation, so `List all conversations` and `Search conversations` are unaffected — their responses return conversation list items, which do not include `conversation_parts`.

## New companies bulk endpoints

### Bulk create companies

You can now [bulk create companies](/docs/references/preview/rest-api/api.intercom.io/companies/bulkcreatecompanies) with `POST /companies/bulk`. This is a create or update: a company is matched on the `company_id` you have defined, mirroring the single create or update company endpoint. It returns a `202` with a job you can poll via `GET /companies/bulk/{id}`.

### Bulk update companies

You can now [bulk update companies](/docs/references/preview/rest-api/api.intercom.io/companies/bulkupdatecompanies) with `PUT /companies/bulk`. Companies are identified by `id`, and an unknown `id` fails that item rather than creating a company. It returns a `202` with a job you can poll via `GET /companies/bulk/{id}`.

### Bulk delete companies

You can now [bulk delete companies](/docs/references/preview/rest-api/api.intercom.io/companies/bulkdeletecompanies) with `POST /companies/bulk/delete`. Companies are identified by `id` and deleted, detaching associated contacts while keeping the company record. It returns a `202` with a job you can poll via `GET /companies/bulk/{id}`.

## Search companies

You can now search for companies with the new [Search companies](/docs/references/preview/rest-api/api.intercom.io/companies/searchcompanies) endpoint (`POST /companies/search`), mirroring [Search contacts](/docs/references/preview/rest-api/api.intercom.io/contacts/searchcontacts). Filter by standard company fields, `custom_attributes.{attribute_name}`, and `tag_id` (which accepts the `=` and `!=` operators). Results use cursor-based pagination via `starting_after`. Companies with no associated users are excluded from results.

## New contacts bulk endpoints

### Bulk create contacts

You can now [bulk create contacts](/docs/references/preview/rest-api/api.intercom.io/contacts/bulkcreatecontacts) with `POST /contacts/bulk`. This is a strict create: a contact matching an existing `external_id` or `email` is rejected rather than updated. It returns a `202` with a job you can poll via `GET /contacts/bulk/{id}`.

### Bulk update contacts

You can now [bulk update contacts](/docs/references/preview/rest-api/api.intercom.io/contacts/bulkupdatecontacts) with `PUT /contacts/bulk`. Contacts are identified by `id`. It returns a `202` with a job you can poll via `GET /contacts/bulk/{id}`.

### Bulk delete contacts

You can now [bulk delete contacts](/docs/references/preview/rest-api/api.intercom.io/contacts/bulkdeletecontacts) with `POST /contacts/bulk/delete`. Contacts are identified by `id` and deleted. It returns a `202` with a job you can poll via `GET /contacts/bulk/{id}`.

## Contacts: `email_domain` field now returned in Contact responses

Contact responses now include the `email_domain` field. It had been documented in the OpenAPI spec but wasn't actually returned in responses; it's now populated as a read-only, computed field.

**New field:**

- `email_domain` (string, nullable) — the domain portion of the contact's `email` address, for example `example.com` for `jane@example.com`. `null` when the contact has no email.


**Affected endpoints:**

- [Get a contact](/docs/references/preview/rest-api/api.intercom.io/contacts/showcontact)
- [Get a contact by External ID](/docs/references/preview/rest-api/api.intercom.io/contacts/showcontactbyexternalid)
- [List contacts](/docs/references/preview/rest-api/api.intercom.io/contacts/listcontacts)
- [Search contacts](/docs/references/preview/rest-api/api.intercom.io/contacts/searchcontacts)
- [Create a contact](/docs/references/preview/rest-api/api.intercom.io/contacts/createcontact)
- [Update a contact](/docs/references/preview/rest-api/api.intercom.io/contacts/updatecontact)


## Set the ticket state when converting a conversation to a ticket

The [`POST /conversations/{id}/convert`](/docs/references/preview/rest-api/api.intercom.io/conversations/convertconversationtoticket) endpoint now accepts an optional `ticket_state_id` parameter, letting you set the ticket's initial state during conversion instead of defaulting to the first state. The `ticket_state_id` must belong to the ticket type passed in `ticket_type_id`.

## Change a ticket's type

You can now change the type of an existing ticket with the new [`POST /tickets/{ticket_id}/change_type`](/docs/references/preview/rest-api/api.intercom.io/tickets/changetickettype) endpoint. The new type must be in the same category as the current type. Attributes that match by name and type are transferred automatically from the old type.

**Request body parameters:**

- `ticket_type_id` (required) — The ID of the new ticket type. Must be in the same category as the current type.
- `ticket_state_id` (required) — The ID of the ticket state for the new ticket type.
- `ticket_attributes` (optional) — Attributes to set on the ticket for the new type. Values provided here override attributes transferred from the old type.


## Conversations: `external_references` to source helpdesk and CRM records

Conversations synced into Intercom from an external platform now expose the records they came from — for example the originating Zendesk ticket or Salesforce case — so you can link an Intercom conversation back to its source system.

**New field on `conversation` and `conversation_list_item`:**

- `external_references` (array) — the external records this conversation is linked to. Each entry has:
  - `type` (string) — the external system the reference points to. Possible values: `zendesk_ticket`, `zendesk_sunshine_conversation`, `salesforce_case`, `salesforce_in_app_message_conversation`, `freshdesk_ticket`, `freshchat_conversation`, `hubspot_conversation`, `custom_helpdesk_conversation`, `api_conversation`.
  - `id` (string) — the record's identifier in that system, always returned as a string.
The array is empty for conversations with no external references, sorted alphabetically by `type`, and capped at 20 entries.


**Affected endpoints:**

- [`GET /conversations/{id}`](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation) — retrieve a single conversation
- [`GET /conversations`](/docs/references/preview/rest-api/api.intercom.io/conversations/listconversations) — list conversations
- [`POST /conversations/search`](/docs/references/preview/rest-api/api.intercom.io/conversations/searchconversations) — search conversations


**Example:**

```json
{
  "external_references": [
    { "type": "zendesk_ticket", "id": "3633338" },
    { "type": "zendesk_sunshine_conversation", "id": "abc-def-uuid" }
  ]
}
```

## Conversations: search by custom conversation attributes

You can now filter `POST /conversations/search` by your workspace's custom conversation attributes, using the field name `custom_attributes.{attribute}` — the same attributes you can search on in the Inbox. Previously only standard conversation fields were searchable through the API.

**Affected endpoint:**

- [`POST /conversations/search`](/docs/references/preview/rest-api/api.intercom.io/conversations/searchconversations) — search conversations


## Set `brand_id` when creating a conversation

You can now set `brand_id` when [creating a conversation](/docs/references/preview/rest-api/api.intercom.io/conversations/createconversation). Multi-brand workspaces can specify the brand at creation time, so email notifications are sent with the correct brand identity. The field is optional; when omitted, the workspace's default brand is used.

## Send WhatsApp messages via the Messages API

You can now send WhatsApp messages through the [Messages API](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/messages/createmessage) by setting `message_type` to `whatsapp`. WhatsApp messages are sent from an approved template: provide the `template` name and its `components`, along with the `from` and `to` recipients.

## Help Center: default flag and supported locales on Help Center responses

Help Center responses now report whether the Help Center is the workspace's default and which locales it supports.

**New fields:**

- `default` (boolean) — whether this Help Center is the workspace's default Help Center.
- `locales` (array of strings) — the locale codes of the languages the Help Center supports, for example `["en", "fr"]`.


**Affected endpoints:**

- [`GET /help_center/help_centers`](/docs/references/preview/rest-api/api.intercom.io/help-center/listhelpcenters) — list every Help Center in the workspace
- [`GET /help_center/help_centers/{id}`](/docs/references/preview/rest-api/api.intercom.io/help-center/retrievehelpcenter) — retrieve a single Help Center


These fields are returned only on the `Preview` version. Stable API versions are unaffected and continue to return the existing Help Center response.

## Conversations: dropped email recipients now returned with a `drop_reason`

Email recipients that Intercom did not deliver to — because the address had bounced, was suppressed, or was otherwise skipped — are now included in conversation responses instead of being silently omitted. Each recipient and email address header carries a new nullable `drop_reason` string explaining why it was dropped; recipients that were delivered to have `drop_reason: null`.

**New field:**

- `drop_reason` (string, nullable) — on each entry of `source.recipients` and of `email_message_metadata.email_address_headers`. `null` for a delivered recipient; otherwise a machine-readable reason such as `user_bounced` (address previously hard-bounced), `user_suppressed` (on the suppression list, with no cause asserted), `user_marked_as_spam` (suppressed after a spam complaint), `user_mailbox_full_suppressed` (suppressed because the mailbox is full), `user_not_contactable` (opted out of email), `email_address_ignored`, or `banned_domain`. Treat the value as an open string — additional reasons may be added over time.


**Affected endpoints:**

- `GET /conversations/{id}`, `PUT /conversations/{id}`, `POST /conversations`, `POST /conversations/{id}/reply` — `source.recipients[].drop_reason` and, on email parts, `conversation_parts[].email_message_metadata.email_address_headers[].drop_reason`
- `GET /conversations`, `POST /conversations/search` — `source.recipients[].drop_reason`
- `GET /tickets/{id}` and other surfaces that embed a conversation source


`drop_reason` is only recorded for emails sent after this feature was enabled; older messages report `null` for every recipient. Stable API versions before this change are unaffected — dropped recipients continue to be filtered out of the response and no `drop_reason` field is returned.

## Conversations: Reply-To addresses on email sources

The `source` object of an email-initiated conversation message now includes a `reply_to` array — the Reply-To header addresses where a reply to the email is actually routed. This can differ from the apparent sender (the `From` address), which is useful for detecting spoofed email where replies would be silently redirected.

## Data event metadata accepts non-string values

The `metadata` object on data events (`POST /events`) now correctly documents that values can be strings, numbers, dates, links, rich links, or monetary amounts — not only strings. The schema previously constrained metadata values to strings, which did not match the API's behaviour. No request or response behaviour has changed; this corrects the schema and the types generated in the SDKs.

## Contacts: mark an email as verified

You can now tell Intercom that a contact has proved they own their email address when you [create](/docs/references/preview/rest-api/api.intercom.io/contacts/createcontact) or [update](/docs/references/preview/rest-api/api.intercom.io/contacts/updatecontact) a contact, using the new `email_verified` field.

Send `email_verified: true` together with `email` in the same request:

```json
{
  "email": "jane@example.com",
  "email_verified": true
}
```

Send `email_verified: false` to record that ownership is not proved. `email_verified` must be sent alongside `email` in the same request; a request that includes `email_verified` without `email` fails with a `400`. Omit the field to leave verification as it is; a newly created contact starts unverified, and changing a verified contact's email leaves the new address unverified until you verify it.

Only set `email_verified: true` when you hold that proof, for example the contact completed a confirmation link or one-time code you sent to the address. Marking an address as verified without proof is a security risk, because Intercom then treats this contact as the owner of that address.

A lead with a verified email is reused when Intercom later matches an inbound email, or an outbound conversation or ticket, to that address, instead of creating a new lead.

## Fin Agent API: content sources on Fin replies

`fin_replied` events now include a `message.sources` array listing the content Fin used to generate its reply, in the same order as the `[n]` citation markers in `message.body`. Each source has a `title`, a `type` (`article` or `external`), and a `url`. The array is present only when Fin cited content.

## Preview a contact merge

You can now [preview a contact merge](/docs/references/preview/rest-api/api.intercom.io/contacts/previewmergecontact) with `POST /contacts/merge/preview` without applying the merge. It's a read-only dry run: send the same `from` (lead) and `into` (user) contact IDs you'd pass to `POST /contacts/merge`, and it reports whether the merge is allowed, without changing anything.

When `allowed` is `true`, the response includes a `summary` of every attribute change (with the resulting value on the surviving contact) and, for each object type, two counts: `from` — how many are on the lead and would move to the user — and `into` — how many the user already has. Counts are reported for conversations, tickets, phone calls, notes, and tags; `conversations` counts non-ticket conversations only, with tickets and phone calls (phone-lead merges only) reported separately. When `allowed` is `false`, it returns `reasons`, each with a stable `code` and a human-readable `message` explaining why.

## Ecommerce catalog upload

The [`POST /ecommerce/connectors/{id}/catalog`](/docs/references/preview/rest-api/api.intercom.io/ecommerce-connectors/uploadecommercecatalog) endpoint is now available. Use it to sync your product catalog to an ecommerce connector.

**Single-file upload** — POST a catalog JSON file in one request:

```bash
curl -X POST "https://api.intercom.io/ecommerce/connectors/{id}/catalog" \
  -H "Authorization: Bearer {access_token}" \
  -H "Intercom-Version: Preview" \
  -F "catalog=@products.json;type=application/json"
```

**Multipart upload** — split large catalogs across multiple requests:

1. POST the first part with `finalize=false`. The response includes an `import_job_id` and `next_part_number`.
2. POST each subsequent part with `finalize=false`, your `import_job_id`, and an incrementing `part_number`.
3. POST a final request with `finalize=true` and your `import_job_id` (no file needed) to trigger the sync.


Each catalog file must be a valid JSON array of objects, up to 99 MB per request. Part uploads are idempotent — retrying the same `part_number` is safe.

## Conversations: link URLs preserved in plaintext (`display_as=plaintext`)

When retrieving a conversation with `display_as=plaintext`, inline links in the conversation source and conversation parts now render their URL alongside the visible label — `label (url)` — instead of dropping the URL. This applies to the `body` and `subject` fields and matches the URLs already present in the HTML representation. The same rendering applies to the plaintext `body` in the `conversation.operator.replied` webhook payload.

**Affected endpoints:**

- `GET /conversations/{id}` (with `display_as=plaintext`)
- `GET /conversations` (with `display_as=plaintext`)
- Webhook topic: `conversation.operator.replied`


## Breaking change: Contact Search date filtering is anchored to UTC

[`POST /contacts/search`](/docs/references/preview/rest-api/api.intercom.io/contacts/searchcontacts) no longer interprets date filters in the workspace timezone:

- **Standard timestamp attributes** filter on the exact UTC second you pass, instead of being widened to the whole workspace-timezone day. A value the API returned now matches that contact exactly, and `>`/`<` comparisons are second-accurate. Affected attributes: `created_at`, `updated_at`, `signed_up_at`, `last_seen_at`, `last_contacted_at`, `last_replied_at`, `last_email_opened_at`, `last_email_clicked_at`, `ios_last_seen_at`, and `android_last_seen_at`.
- **Customer-defined date attributes** are anchored to UTC calendar days rather than workspace-timezone calendar days.


The standard timestamp attributes listed above are also reported as `datetime` instead of `date` by [`GET /data_attributes`](/docs/references/preview/rest-api/api.intercom.io/data-attributes/lisdataattributes). Customer-defined date attributes are still reported as `date`.

Migrating from an earlier version:

- **Standard timestamp attributes** — a `=` filter that previously matched anything on a given day now matches only that exact second. To keep matching a whole day, bracket it with `>` and `<`; both are exclusive, so pass the second before the day starts and the first second of the next day.
- **Customer-defined date attributes** — `=` still matches the whole day, so existing filters keep working. Only the day boundary moves, from your workspace timezone to UTC.


All stable API versions (2.16 and earlier) are unaffected and continue to bucket date filters by the whole workspace-timezone day.

## Breaking change: conversation `priority` returns granular levels

The conversation `priority` field now returns one of `none`, `low`, `medium`, `high`, or `urgent` instead of the binary `priority` / `not_priority` values. The `current_priority` and `previous_priority` fields on the `priority_changed` conversation part use the same values.

Migrating from an earlier version: `not_priority` maps to `none` and `priority` maps to `high`, with `urgent` added as a new tier above `high`. All stable API versions (2.15 and earlier) are unaffected and continue to return `priority` / `not_priority`.

## `attachment_files` on ticket reply for admin senders

The [`POST /tickets/{id}/reply`](/docs/references/preview/rest-api/api.intercom.io/tickets/replyticket) endpoint now accepts `attachment_files` for admin replies (`type: admin`). Each entry requires:

| Field | Type | Description |
|  --- | --- | --- |
| `data` | string | Base64-encoded file content |
| `name` | string | Filename including extension |
| `content_type` | string | MIME type (e.g. `application/pdf`) |


Up to 10 files per request. When both `attachment_files` and `attachment_urls` are provided, `attachment_files` takes precedence.

## Breaking change: `POST /contacts/search` now validates `sort.order`

When sorting [Search contacts](/docs/references/preview/rest-api/api.intercom.io/contacts/searchcontacts) results, `sort.order` now accepts only `ascending` or `descending`. Any other value returns **HTTP 400 Bad Request** instead of silently falling back to `descending`:

```json
{
  "type": "error.list",
  "errors": [{ "code": "invalid_sort_order", "message": "Invalid sort order 'desc'. Must be one of: ascending, descending" }]
}
```

Pass a `sort` object with a valid `order` to sort your results:

```json
{
  "query": { "field": "created_at", "operator": ">", "value": "1306054154" },
  "sort": { "field": "created_at", "order": "ascending" }
}
```

`sort.order` defaults to `descending` when omitted. All stable API versions (2.15 and earlier) are unaffected and continue to accept any `sort.order` value.

## Breaking change: `POST /contacts/merge` rejects contacts that are not duplicates

[Merging a lead into a user](/docs/references/preview/rest-api/api.intercom.io/contacts/mergecontact) now requires the two contacts to be duplicates: they must share a matching email address or phone number. A request to merge two contacts that share no matching identifier returns **HTTP 400 Bad Request**:

```json
{
  "type": "error.list",
  "errors": [
    {
      "code": "invalid_merge",
      "message": "Contacts can only be merged when they are duplicates (matching email or phone). Pass skip_duplicate_validation=true to override this check."
    }
  ]
}
```

To merge two contacts that are not duplicates, set `skip_duplicate_validation` to `true` in the request body:

```json
{
  "from": "5d70dd30de4efd54f42fd526",
  "into": "5ba682d23d7cf92bef87bfd4",
  "skip_duplicate_validation": true
}
```

## Link conversations to tracker tickets

You can now link an existing conversation or customer ticket to a tracker ticket after the tracker has been created, and unlink it later.

**New endpoints:**

- `POST /tickets/{ticket_id}/linked_conversations` — Link a conversation to a tracker ticket
- `DELETE /tickets/{ticket_id}/linked_conversations/{id}` — Unlink a conversation from a tracker ticket


The ticket in the path must be a tracker ticket. Pass the conversation (or customer ticket) to link as `conversation_id` in the request body. A conversation can be linked to one tracker ticket at a time.

## Reporting Data Export API now includes no-reply conversations

On the Preview version, the Reporting Data Export API now returns **all** conversations for the `conversation` dataset, including those that never received a user reply. Earlier versions return only conversations with at least one user reply. This applies to `POST /export/reporting_data/enqueue`.

This is a breaking change to the conversation population, gated to the Preview version. Existing integrations on dated versions are unaffected until they adopt the version that carries this change.

## Tag Knowledge Hub content

Preview only
These endpoints are available in the Preview API version only. They are not yet included in any stable API version.

You can now apply and remove an existing tag on Knowledge Hub content — articles, content snippets, and internal articles — through the API. The tag must already exist in the workspace (create tags with the Tags API), and the authenticating teammate must have the manage knowledge base content permission. These endpoints require the `Intercom-Version: Preview` header.

- [`POST /articles/{article_id}/tags`](/docs/references/preview/rest-api/api.intercom.io/articles/attachtagtoarticle) — Apply a tag to an article. Requires the `read_write_articles_scope` OAuth scope.
- [`DELETE /articles/{article_id}/tags/{id}`](/docs/references/preview/rest-api/api.intercom.io/articles/detachtagfromarticle) — Remove a tag from an article. Requires the `read_write_articles_scope` OAuth scope.
- [`POST /content_snippets/{content_snippet_id}/tags`](/docs/references/preview/rest-api/api.intercom.io/content-snippets/attachtagtocontentsnippet) — Apply a tag to a content snippet. Requires the `read_write_content_snippets` OAuth scope.
- [`DELETE /content_snippets/{content_snippet_id}/tags/{id}`](/docs/references/preview/rest-api/api.intercom.io/content-snippets/detachtagfromcontentsnippet) — Remove a tag from a content snippet. Requires the `read_write_content_snippets` OAuth scope.
- [`POST /internal_articles/{internal_article_id}/tags`](/docs/references/preview/rest-api/api.intercom.io/internal-articles/attachtagtointernalarticle) — Apply a tag to an internal article. Requires the `read_write_articles_scope` OAuth scope.
- [`DELETE /internal_articles/{internal_article_id}/tags/{id}`](/docs/references/preview/rest-api/api.intercom.io/internal-articles/detachtagfrominternalarticle) — Remove a tag from an internal article. Requires the `read_write_articles_scope` OAuth scope.


The tag is returned with `applied_at` and `applied_by` populated when applied, and with both set to `null` when removed.

Internal article read responses now also include a `tags` field — [`GET /internal_articles`](/docs/references/preview/rest-api/api.intercom.io/internal-articles/listinternalarticles) and [`GET /internal_articles/{id}`](/docs/references/preview/rest-api/api.intercom.io/internal-articles/retrieveinternalarticle) return the tags currently applied to each internal article.

This change is available in the Preview API version only.

## Bulk tag Knowledge Hub content

The [`POST /content/bulk_actions`](/docs/references/preview/rest-api/api.intercom.io/content/bulkcontentactions) endpoint now supports an `update_tags` action to apply and/or remove existing tags across Knowledge Hub content in a single call.

Provide a `tags` object with `add_tag_ids` and/or `remove_tag_ids` (supply at least one). Tags must already exist and not be archived (create tags with the Tags API); unknown or archived tag IDs are rejected with `parameter_invalid` (HTTP 422). Applying and removing are independent — this is not a replace.

`update_tags` accepts `article`, `content_snippet`, `external_content`, `file_source_content`, and `internal_article` content types. Unlike the other bulk actions, it addresses articles by the parent **`article`** id, not `article_content` — using `article_content` is silently dropped.

Requires the `write_content` OAuth scope. Set `Intercom-Version: Preview`.

## Help Center Redirects API

You can now manage a help center's URL redirects through the API. A redirect maps a `from_url` to an article or collection, so links to old or external URLs resolve to live content — useful when migrating from another knowledge base or restructuring article URLs.

These endpoints require the `Intercom-Version: Preview` header. Listing and reading redirects works with the `read_help_center_redirects_scope` OAuth scope; creating and deleting require the `read_write_help_center_redirects_scope` OAuth scope.

- [`GET /help_center/help_centers/{help_center_id}/redirects`](/docs/references/preview/rest-api/api.intercom.io/help-center/listhelpcenterredirects) — List all redirects for a help center
- [`POST /help_center/help_centers/{help_center_id}/redirects`](/docs/references/preview/rest-api/api.intercom.io/help-center/createhelpcenterredirect) — Create a redirect
- [`GET /help_center/help_centers/{help_center_id}/redirects/{id}`](/docs/references/preview/rest-api/api.intercom.io/help-center/retrievehelpcenterredirect) — Retrieve a redirect
- [`DELETE /help_center/help_centers/{help_center_id}/redirects/{id}`](/docs/references/preview/rest-api/api.intercom.io/help-center/deletehelpcenterredirect) — Delete a redirect


The `from_url` must be an absolute URL within the help center's URL space, and a redirect's target (article or collection) must belong to the help center.

## Schedule Article publishing and unpublishing

You can now schedule when an article publishes and when it unpublishes. Two new optional fields have been added to the Articles API:

- `scheduled_publish_at` — schedule a future publish for the article. Combine with `state: "published"` to schedule the article instead of publishing it immediately. Setting it to `null` cancels a pending publish schedule. Combining with `state: "draft"` returns a 400 error.
- `scheduled_unpublish_at` — schedule a future unpublish for the article. Setting it to `null` cancels a pending unpublish schedule. The article must have been published at least once.


Only one pending schedule can exist per article at a time — `scheduled_publish_at` and `scheduled_unpublish_at` are mutually exclusive in the same request. Both fields are `null` when no schedule is pending.

On requests, the fields accept an ISO 8601 timestamp string (for example, `"2026-12-31T09:00:00Z"`). On responses, they are returned as Unix timestamps in seconds (or `null`).

Timestamps must be strictly in the future — values in the past or equal to the current time are rejected with 400 `parameter_invalid`.

The fields are accepted on the request body of [`POST /articles`](/docs/references/preview/rest-api/api.intercom.io/articles/createarticle) and [`PUT /articles/{id}`](/docs/references/preview/rest-api/api.intercom.io/articles/updatearticle), and returned by [`GET /articles`](/docs/references/preview/rest-api/api.intercom.io/articles/listarticles), [`GET /articles/{id}`](/docs/references/preview/rest-api/api.intercom.io/articles/retrievearticle), [`GET /articles/search`](/docs/references/preview/rest-api/api.intercom.io/articles/searcharticles), [`POST /articles`](/docs/references/preview/rest-api/api.intercom.io/articles/createarticle), and [`PUT /articles/{id}`](/docs/references/preview/rest-api/api.intercom.io/articles/updatearticle).

This change is available in the Preview API version only.

## Article version history

Preview only
These endpoints are available in the Preview API version only. They are not yet included in any stable API version.

You can now read the version history of an article through the API. Versions are returned newest-first, and you can retrieve any prior version's full content (`title`, `body`, `body_markdown`) without affecting the live article.

These endpoints require the `Intercom-Version: Preview` header and the `read_articles_scope` OAuth scope.

- [`GET /articles/{article_id}/versions`](/docs/references/preview/rest-api/api.intercom.io/articles/listarticleversions) — List version metadata for an article (newest-first, paginated).
- [`GET /articles/{article_id}/versions/{id}`](/docs/references/preview/rest-api/api.intercom.io/articles/retrievearticleversion) — Retrieve a single version with its full content.


Both endpoints accept an optional `locale` query parameter to scope the response to a specific locale.

## Search Knowledge Hub content

Preview only
This endpoint is available in the Preview API version only. It is not yet included in any stable API version.

You can now search across all Knowledge Hub content types — articles, content snippets, external content, file source content, and internal articles — in a single call with [`GET /content/search`](/docs/references/preview/rest-api/api.intercom.io/content/searchcontent). The endpoint requires the `Intercom-Version: Preview` header and the `read_content` OAuth scope.

You can narrow results with filters for publication state, locale, tags, folders, content type, AI usage (Copilot, Fin AI Agent, and Fin Sales Agent), admin attribution (created by and last updated by), and creation or update date ranges.

## Content import source webhook topics

Preview only
These webhook topics are available in the Preview API version only. They are not yet included in any stable API version.

You can now subscribe to webhook notifications for content import source lifecycle events. Three new topics are available:

- `content_import_source.created` — fires when a content import source is created
- `content_import_source.updated` — fires when a content import source's URL, sync behavior, or status changes
- `content_import_source.deleted` — fires when a content import source is deleted


All three topics require the **Read content** permission. The `content_import_source.created` and `content_import_source.updated` payloads contain the full content import source object; `content_import_source.deleted` sends a minimal identification payload. See the [webhook topics reference](/docs/references/preview/webhooks/webhook-models) for details.

## Article drafts

You can now manage the draft lifecycle of a published article through the API. Staging changes as a draft leaves the live article untouched until you choose to publish.

A published article that has staged edits reports `has_unpublished_changes: true` along with a `draft_updated_at` timestamp, so you can detect pending changes before reading or publishing them.

These endpoints require the `Intercom-Version: Preview` header. Reading a draft works with the `read_articles_scope` OAuth scope; staging and publishing require the `read_write_articles_scope` OAuth scope.

- [`GET /articles/{id}/draft`](/docs/references/preview/rest-api/api.intercom.io/articles/retrievearticledraft) — Retrieve a staged draft
- [`PUT /articles/{id}/draft`](/docs/references/preview/rest-api/api.intercom.io/articles/stagearticledraft) — Stage a draft
- [`POST /articles/{id}/draft/publish`](/docs/references/preview/rest-api/api.intercom.io/articles/publisharticledraft) — Publish a staged draft


On a multilingual workspace, publishing requires a `locales` array naming which locales to publish; on a single-language workspace the body can be omitted.

## Orchestrate Fin with new Fin Agent API endpoints

You can now drive Fin from your own orchestrating agent with four new endpoints:

- [`POST /fin/capabilities`](/docs/references/preview/rest-api/api.intercom.io/fin-agent/listfincapabilities) — discover the per-user list of what Fin can do, so your agent can decide which endpoint to call.
- [`POST /fin/ask`](/docs/references/preview/rest-api/api.intercom.io/fin-agent/askfin) — ask Fin a single, self-contained question and receive one informational answer (non-conversational).
- [`POST /fin/procedures/{procedure_id}/run`](/docs/references/preview/rest-api/api.intercom.io/fin-agent/runfinprocedure) — deterministically run a specific procedure on a new conversation.
- [`POST /fin/escalate`](/docs/references/preview/rest-api/api.intercom.io/fin-agent/escalatefinconversation) — hand a conversation off to a human teammate.


Continue a procedure conversation with [`POST /fin/reply`](/docs/references/preview/rest-api/api.intercom.io/fin-agent/replytofin): when the conversation's status is `awaiting_user_reply`, send the user's response there.

## Banners API

You can now retrieve the banners a contact matches and record dismissals through the new Banners API. This lets you display banners on surfaces outside the Messenger, such as native mobile apps, kiosks, and embedded tools, while keeping dismissal state in sync with the web Messenger.

These endpoints require the `Intercom-Version: Preview` header and an OAuth token with the `read_write_users` scope.

- [`GET /contacts/{id}/banners`](/docs/references/preview/rest-api/api.intercom.io/banners/listcontactbanners) — List the banners a contact currently matches
- [`POST /contacts/{id}/banners/{view_id}/dismiss`](/docs/references/preview/rest-api/api.intercom.io/banners/dismisscontactbanner) — Dismiss a banner for a contact


Requesting a contact's banners records an impression for each banner returned, so call the endpoint at the point you're about to display them. Banners that rely on client-side targeting rules (such as page URL or time on page) aren't returned by this endpoint.

## Office Hours API

You can now manage office hours schedules and their exceptions through the new Office Hours API. Schedules define the recurring weekly hours your workspace is open; exceptions override those hours on specific dates (for example, public holidays).

These endpoints require an OAuth token with the `read_write_office_hours` scope.

**Schedules**

- [`GET /office_hours_schedules`](/docs/references/preview/rest-api/api.intercom.io/office-hours/listofficehoursschedules) — List all schedules
- [`POST /office_hours_schedules`](/docs/references/preview/rest-api/api.intercom.io/office-hours/createofficehoursschedule) — Create a schedule
- [`GET /office_hours_schedules/{id}`](/docs/references/preview/rest-api/api.intercom.io/office-hours/getofficehoursschedule) — Retrieve a schedule
- [`PUT /office_hours_schedules/{id}`](/docs/references/preview/rest-api/api.intercom.io/office-hours/updateofficehoursschedule) — Update a schedule
- [`DELETE /office_hours_schedules/{id}`](/docs/references/preview/rest-api/api.intercom.io/office-hours/deleteofficehoursschedule) — Delete a schedule


**Exceptions**

- [`GET /office_hours_schedules/{office_hours_schedule_id}/office_hours_exceptions`](/docs/references/preview/rest-api/api.intercom.io/office-hours/listofficehoursexceptions) — List exceptions for a schedule
- [`POST /office_hours_schedules/{office_hours_schedule_id}/office_hours_exceptions`](/docs/references/preview/rest-api/api.intercom.io/office-hours/createofficehoursexception) — Create an exception
- [`GET /office_hours_schedules/{office_hours_schedule_id}/office_hours_exceptions/{id}`](/docs/references/preview/rest-api/api.intercom.io/office-hours/getofficehoursexception) — Retrieve an exception
- [`PUT /office_hours_schedules/{office_hours_schedule_id}/office_hours_exceptions/{id}`](/docs/references/preview/rest-api/api.intercom.io/office-hours/updateofficehoursexception) — Update an exception
- [`DELETE /office_hours_schedules/{office_hours_schedule_id}/office_hours_exceptions/{id}`](/docs/references/preview/rest-api/api.intercom.io/office-hours/deleteofficehoursexception) — Delete an exception


## Audience webhook topics

Preview only
The Audiences API and these webhook topics are available in the Preview API version only. They are not yet included in any stable API version.

You can now subscribe to webhook notifications for audience lifecycle and membership events. Five new topics are available:

- `audience.created` — fires when an audience is created
- `audience.updated` — fires when an audience's name, predicates, or role predicates change
- `audience.deleted` — fires when an audience is deleted
- `audience.member_added` — fires when a piece of content is added as a member of an audience
- `audience.member_removed` — fires when a piece of content is removed from an audience


All five topics require the **Read audiences** permission. The `audience.created` and `audience.updated` payloads contain the full audience object; `audience.deleted` sends a minimal identification payload; and the `audience.member_*` payloads contain an `audience_membership` object describing the content entity and the audience it belongs to. See the [webhook topics reference](/docs/references/preview/webhooks/webhook-models) for details.

## Help Center audience added to Articles

A new optional, read-only field has been added to the Articles API:

- `help_center_audience` (string) — the audience that can view the article in the Help Center: one of `everyone`, `all_users`, `all_visitors`, `all_leads`, `all_visitors_and_leads`, or `restricted` (a custom audience ruleset). For multilingual articles this is the article-level audience, and it is `null` when no audience is configured.


This field is returned by [`GET /articles`](/docs/references/preview/rest-api/api.intercom.io/articles/listarticles), [`GET /articles/{id}`](/docs/references/preview/rest-api/api.intercom.io/articles/retrievearticle), [`GET /articles/search`](/docs/references/preview/rest-api/api.intercom.io/articles/searcharticles), [`POST /articles`](/docs/references/preview/rest-api/api.intercom.io/articles/createarticle), [`PUT /articles/{id}`](/docs/references/preview/rest-api/api.intercom.io/articles/updatearticle), and the `article.*` webhook topics. It is read-only — set the audience through the Articles UI in Intercom.

## Fin statistics added to article statistics

Three new optional, read-only fields have been added to the `statistics` object on Article responses:

- `fin_involvements` (integer) — the number of conversations in which Fin AI Agent used the article, summed across all of the article's locales.
- `fin_resolutions` (integer) — the number of those conversations that Fin AI Agent resolved using the article, summed across all of the article's locales.
- `fin_resolution_rate` (number) — the percentage of Fin involvements that resulted in a resolution (`fin_resolutions` / `fin_involvements` * 100).


These fields are returned in the article `statistics` object by [`GET /articles/{id}`](/docs/references/preview/rest-api/api.intercom.io/articles/retrievearticle), [`POST /articles`](/docs/references/preview/rest-api/api.intercom.io/articles/createarticle), and [`PUT /articles/{id}`](/docs/references/preview/rest-api/api.intercom.io/articles/updatearticle). They are read-only and reflect Fin AI Agent reporting.

## Audit and suggestion fields added to Articles

Three new optional, read-only fields have been added to the Articles API:

- `created_by_id` (integer) — the ID of the teammate who created the content. At the article root, this is the creator of the default language's content; inside each per-locale entry in `translated_content`, it is the creator of that content version.
- `updated_by_id` (integer) — the ID of the teammate who last updated the content. At the article root, this is the last editor of the default language's content; inside each per-locale entry in `translated_content`, it is the last editor of that content version.
- `exclude_from_article_suggestions` (boolean) — whether the article is excluded from Fin AI Agent article suggestions. Returned at the article root only.


These fields are returned by [`GET /articles`](/docs/references/preview/rest-api/api.intercom.io/articles/listarticles), [`GET /articles/{id}`](/docs/references/preview/rest-api/api.intercom.io/articles/retrievearticle), [`GET /articles/search`](/docs/references/preview/rest-api/api.intercom.io/articles/searcharticles), [`POST /articles`](/docs/references/preview/rest-api/api.intercom.io/articles/createarticle), [`PUT /articles/{id}`](/docs/references/preview/rest-api/api.intercom.io/articles/updatearticle), and the `article.*` webhook topics. They are read-only — set the underlying values through the Articles UI in Intercom.

## Conversations API: include QA monitor evaluations and scorecards

The Conversations API can now return QA monitor evaluations and scorecard results on conversation responses when you opt in. Two new query parameters control which fields are included:

- `include_monitors=true` — adds a `monitor_evaluations` array listing any QA monitors that flagged the conversation.
- `include_scorecards=true` — adds a `scorecards` array with scorecard results, including per-evaluator outcomes and the reviewed teammate (Fin or a specific admin).


Both parameters are available on:

- [`GET /conversations`](/docs/references/preview/rest-api/api.intercom.io/conversations/listconversations)
- [`GET /conversations/{id}`](/docs/references/preview/rest-api/api.intercom.io/conversations/retrieveconversation)
- [`POST /conversations/search`](/docs/references/preview/rest-api/api.intercom.io/conversations/searchconversations)


When the corresponding parameter is omitted or `false`, the response is unchanged. Each array is empty if the conversation has no QA evaluations.

## Complete Audiences API

You can now create, retrieve, update, and delete audiences through the Audiences API.

**New endpoints:**

- `GET /audiences/{id}` — Retrieve a single audience
- `POST /audiences` — Create a new audience
- `PUT /audiences/{id}` — Update an existing audience
- `DELETE /audiences/{id}` — Delete an audience


**Updated:**

- `GET /audiences` — Response now includes `predicates` and `role_predicates` on each audience object


The `audience` object now includes `predicates` and `role_predicates` arrays, which define the matching rules for the audience. Each predicate contains `attribute`, `type`, `comparison`, and `value` fields.

## Contacts: `merge_history` field and `GET /contacts/{id}/merge_history` endpoint added

You can now retrieve the merge history of a contact — the list of contacts that were merged into it.

Pass `include_merge_history=true` as a query parameter on any of the following endpoints to include a `merge_history` array in the response:

- [Get a contact](/docs/references/preview/rest-api/api.intercom.io/contacts/showcontact)
- [Get a contact by External ID](/docs/references/preview/rest-api/api.intercom.io/contacts/showcontactbyexternalid)
- [List contacts](/docs/references/preview/rest-api/api.intercom.io/contacts/listcontacts)
- [Search contacts](/docs/references/preview/rest-api/api.intercom.io/contacts/searchcontacts)
- [Update a contact](/docs/references/preview/rest-api/api.intercom.io/contacts/updatecontact)
- [Merge a lead and a user](/docs/references/preview/rest-api/api.intercom.io/contacts/mergecontact) — returns the merge history of the resulting target contact


Only returned for contacts with a `user` role. Each entry contains:

- `type` — `"merge_history"`
- `source_contact_id` — the Intercom ID of the contact that was merged in
- `source_contact_role` — the role (`lead` or `user`) of the merged contact
- `merged_at` — UNIX timestamp of when the merge occurred (nullable)


[GET /contacts/{id}/merge_history](/docs/references/preview/rest-api/api.intercom.io/contacts/listcontactmergehistory) also returns a paginated list of merge history entries. Supports `cursor`, `per_page`, and `order` query parameters. Only available for contacts with a `user` role.

## Breaking change: Merged contacts now return 410 Gone instead of 404

After a merge, requesting the source contact by its original ID now returns **HTTP 410 Gone** instead of `404 Not Found`, with a `Link` response header pointing to the canonical (merged-into) contact:

```
Link: </contacts/{canonical_id}>; rel="canonical"
```

The response body contains:

```json
{
  "type": "error.list",
  "errors": [{ "code": "contact_merged", "message": "This contact has been merged. See the 'Link' header for the canonical contact." }]
}
```

The `Link` header resolves multi-hop merge chains (up to 3 hops) to return the final target contact. This applies to [Get a contact](/docs/references/preview/rest-api/api.intercom.io/contacts/showcontact) and lookups by `external_id`. Merged contacts continue to be excluded from [List contacts](/docs/references/preview/rest-api/api.intercom.io/contacts/listcontacts) and [Search contacts](/docs/references/preview/rest-api/api.intercom.io/contacts/searchcontacts) results.

## Agent availability fields added to Content Snippets, Internal Articles, Articles, and External Pages

Boolean fields `ai_chatbot_availability`, `ai_copilot_availability`, and `ai_sales_agent_availability` are now available on the following resources:

- **Content Snippets** — Added to `GET`, `POST`, and `PUT` responses and request bodies. These fields let you control whether a content snippet is available to AI Chatbot (Fin), AI Copilot, or AI Sales Agent.
- **Internal Articles** — Added to `GET`, `POST`, and `PUT` responses and request bodies.
- **Articles** — Added to `POST` and `PUT` request bodies (already present in responses).
- **External Pages** — `ai_sales_agent_availability` added to `GET` responses and `POST`/`PUT` request bodies. `ai_agent_availability` and `ai_copilot_availability` added to `PUT` request bodies (already present in `GET` responses and `POST` requests).


## Activity Log: `hide_csat_from_agents_setting_change` event type added

The `hide_csat_from_agents_setting_change` event type has been added to the Activity Log event types enum. This event is recorded when a workspace admin enables or disables the "Hide CSAT scores from agents" setting.

There are [breaking changes](/docs/build-an-integration/learn-more/rest-apis/api-changelog#about-breaking-changes-in-the-intercom-api) in this version, which are detailed below.

## Content Snippets, Internal Articles & AI External Pages: `audience_ids` field

Content snippets, internal articles, and AI external pages can now be targeted to specific Fin AI Agent audiences via the API. (Articles already supported this — see the entry further down.)

**New field on responses** (`content_snippet`, `internal_article`, `external_page`):

- `audience_ids` (array of integers, nullable) — the audience IDs the content item is targeted to. Empty array means no targeting is set.


**New field on write requests** (`POST` and `PUT` on each resource):

- `audience_ids: [id1, id2]` — sets audience targeting for the content item
- On `PUT`, omitting the field leaves memberships intact (PATCH semantics); `audience_ids: []` clears all memberships
- On `POST /content_snippets` and `POST /internal_articles`, omitting the field creates the item with no audience targeting
- On `POST /ai/external_pages`, omitting the field preserves defaults inherited from the parent content import source; an explicit array overrides those defaults; `[]` clears all targeting (even when the source has defaults)
- Unknown audience IDs return a `404` error with no partial commit


Unlike multilingual help-center articles (which carry audience targeting per-locale inside `translated_content`), these resources are single-entity and use a flat top-level `audience_ids` field.

**Affected endpoints:**

- `GET /content_snippets`, `GET /content_snippets/{id}`, `POST /content_snippets`, `PUT /content_snippets/{id}`
- `GET /internal_articles`, `GET /internal_articles/{id}`, `POST /internal_articles`, `PUT /internal_articles/{id}`
- `GET /ai/external_pages`, `GET /ai/external_pages/{id}`, `POST /ai/external_pages`, `PUT /ai/external_pages/{id}`


## Dedicated Conversation Attributes API

You can now create, update, and archive conversation attributes through dedicated endpoints at `/conversations/attributes`. Responses include type-specific fields based on `data_type`.

**New endpoints:**

- `GET /conversations/attributes` — List all conversation attributes
- `GET /conversations/attributes/{id}` — Get a single conversation attribute
- `POST /conversations/attributes` — Create a new conversation attribute
- `PUT /conversations/attributes/{id}` — Update a conversation attribute
- `DELETE /conversations/attributes/{id}` — Archive a conversation attribute (soft delete)
- `POST /conversations/attributes/{id}/options` — Add an option to a list attribute
- `PUT /conversations/attributes/{id}/options/{option_id}` — Rename a list option
- `DELETE /conversations/attributes/{id}/options/{option_id}` — Archive a list option


**Type-specific fields:**

- **String** — includes `multiline` (boolean)
- **List** — includes `options` (array of option objects, each with `id` UUID, `label`, and `archived`)
- **Relationship** — includes `reference` object with `type` (one/many) and `object_type_id`
- **Other types** — return base shape only


**List option management:**

List options are managed individually through the nested options endpoints rather than by replacing the full array on `PUT /conversations/attributes/{id}`. Each option object includes a UUID (`id`) that is used as the `option_id` path parameter when updating or archiving. A list attribute must always have at least 2 active (non-archived) options.

## Breaking change: `GET /data_attributes` no longer returns conversation attributes

`GET /data_attributes` (with or without `model=conversation`) no longer returns conversation attributes in the Preview API version. Use [GET /conversations/attributes](/docs/references/preview/rest-api/api.intercom.io/conversations-attributes/listconversationattributes) instead.

- Calling `GET /data_attributes` without a `model` parameter no longer includes conversation attributes in the response.
- Calling `GET /data_attributes?model=conversation` returns a `422` error:


```json
{
  "type": "error.list",
  "errors": [
    {
      "code": "parameter_invalid",
      "message": "model=conversation is no longer supported. Use GET /conversations/attributes instead"
    }
  ]
}
```

All stable API versions (2.15, 2.14, etc.) continue to return conversation attributes from this endpoint unchanged.

## Conversations API: `channel` field for channel mix reporting

The `GET /conversations/{id}`, `GET /conversations`, and `POST /conversations/search` endpoints now include a `channel` object on each conversation, exposing which channel the conversation originated from and its current channel.

This makes it possible to accurately report on channel mix.

**New field on `conversation` and `conversation_list_item`:**

- `channel` (object, nullable) — channel metadata for the conversation. Returns `null` if channel data is unavailable.
  - `initial` (string, nullable) — the channel through which the conversation was originally initiated. Possible values: `messenger`, `zendesk_sunshine`, `zendesk_ticket`, `twitter`, `email`. `null` if channel data is unavailable.
  - `current` (string, nullable) — the current channel of the conversation. May differ from `initial` if the conversation was migrated between channels. `null` if channel data is unavailable.


**Example:**

```json
{
  "channel": {
    "initial": "zendesk_sunshine",
    "current": "zendesk_sunshine"
  }
}
```

## Conversations: `POST /conversations/{id}/merge` added

You can now merge a secondary conversation into a primary conversation via the API. The secondary conversation is closed and linked to the primary, which becomes the surviving thread. Returns the primary conversation on success.

Requires `write_conversations` OAuth scope. When the secondary is a ticket, `write_tickets` scope is also required.

## Articles: `audience_ids` field

Articles can now be targeted to specific Fin AI Agent audiences via the API.

**New field on responses** (`article_content`):

- `audience_ids` (array of integers) — the audience IDs this article content is targeted to. On multilingual help centers this appears per-locale inside `translated_content`. On single-language help centers it appears at the article root level. Empty array means no targeting is set.


**New field on write requests** (`POST /articles`, `PUT /articles/{id}`):

- Top-level `audience_ids: [id1, id2]` — broadcasts the same set to every locale
- `translated_content.<locale>.audience_ids` — targets a specific locale without affecting others
- When both are sent in the same request, top-level wins
- `audience_ids: []` on `PUT` clears all audience memberships from every locale
- Unknown audience IDs return a `404` error with no partial commit


**Affected endpoints:**

- `GET /articles`, `GET /articles/{id}`
- `POST /articles`, `PUT /articles/{id}`


## Articles, Internal Articles & Content Snippets: `body_markdown` field

All knowledge API endpoints now support a `body_markdown` field for reading and writing content in Markdown format, alongside the existing HTML `body` and `json_blocks` fields.

**GET responses** include `body_markdown` — the article content serialized as Markdown. Standard blocks (headings, paragraphs, lists, code, images, tables) render as native GFM Markdown. Rich blocks (callouts, video embeds, buttons, collapsible sections, attachments, etc.) render as CommonMark `:::` directives (e.g. `:::button label="Sign up" href="..." :::`) for lossless round-tripping.

**POST/PUT requests** accept `body_markdown` as an alternative to `body` or `json_blocks`. The fields are mutually exclusive — providing more than one returns `422`.

**Affected endpoints:**

- `GET /articles`, `GET /articles/{id}`, `GET /articles/search`
- `POST /articles`, `PUT /articles/{id}`
- `GET /internal_articles`, `GET /internal_articles/{id}`
- `POST /internal_articles`, `PUT /internal_articles/{id}`
- `GET /content_snippets`, `GET /content_snippets/{id}`
- `POST /content_snippets`, `PUT /content_snippets/{id}`


## Audiences API

The Audiences API is now available in the Preview version. You can list all audiences configured for your workspace using `GET /audiences`. The response is a paginated list of audience objects, each containing `id`, `name`, `created_at`, and `updated_at` fields. This endpoint requires the `read_audiences` OAuth scope.

## Call timing metrics

The [Calls API](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Calls/showCall) now returns four new timing fields on call objects:

- `duration` — Total call duration in seconds from the caller's perspective.
- `talk_time` — Total time in seconds the agent and customer were connected.
- `queue_time` — Total time in seconds the caller waited in queue before connecting.
- `hold_time` — Total time in seconds the caller was placed on hold during the call.


All four fields are nullable integers and are available on both `GET /calls/{id}` and `GET /calls` endpoints.

## Conversations API: source field accuracy improved

The `source` field on conversation objects was returning `null` for some conversations — including those started by an admin sending an email, outbound phone calls, and conversations created via third-party integrations like Zendesk Tickets or Salesforce Cases.

## POST /tags response status

`POST /tags` no longer returns `404` when a referenced user or company does not exist. Instead, the tag is created (or updated), valid entities are linked and missing entities are skipped.

The response now includes two new fields: `users` and `companies`, list of objects containing:

- `id`: the Intercom ID of the entity (string)
- `tagged`: `true` if the entity was tagged, `false` if untagged


**Example response (company not found):**

```json
{
  "type": "tag",
  "id": "789",
  "name": "VIP",
  "users": [],
  "companies": []
}
```

**Example response (one company tagged, one missing):**

```json
{
  "type": "tag",
  "id": "789",
  "name": "VIP",
  "users": [],
  "companies": [{ "id": "valid-123", "tagged": true }]
}
```

This fixes a long-standing issue where `POST /tags` would create an orphaned tag before validating that entities exist — the tag was committed but no entities were linked.

## Content snippet webhook topics

Preview only
The Content Snippets API is available in the Preview API version only. It is not yet included in any stable API version.

You can now subscribe to webhook notifications for content snippet lifecycle events. Three new topics are available:

- `content_snippet.created` — fires when a content snippet is created
- `content_snippet.updated` — fires when a content snippet's content or metadata is updated
- `content_snippet.deleted` — fires when a content snippet is deleted


These topics require the `Read content snippets` permission. See the [webhook topics reference](/docs/references/preview/webhooks/webhook-models) for full details.

## Delete content snippets via API

Preview only
The Content Snippets API is available in the Preview API version only. It is not yet included in any stable API version.

You can now delete content snippets through the API using `DELETE /content_snippets/{id}`. This completes the full CRUD surface for content snippets, which previously supported list, get, create, and update operations.

The endpoint returns `204 No Content` on success. If the snippet is referenced by Fin Procedures, the request returns `422` with error code `content_has_procedure_dependencies`.

## Procedures HITL API

A new `POST /procedures/{conversation_id}/collect_agent_input` endpoint enables external systems to submit human-collected input to Fin Procedures paused at a Human in the Loop (HITL) step.

**New Webhook Topic:**

- `procedure.hitl_notification.created` — Fires when a Fin Procedure reaches a HITL step with the API channel enabled. Delivers conversation context, the HITL question, attributes to collect, and a callback URL.


**New Endpoint:**

- [`POST /procedures/{conversation_id}/collect_agent_input`](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Procedures/collectProcedureAgentInput) — Submit attribute values collected by your system to resume the procedure.


**Requirements:**

- Preview API version
- OAuth app with `write_conversations` scope (callback) and `Read conversations` permission (webhook)
- Procedures HITL API feature enabled for your workspace


## Article webhook topics

You can now subscribe to webhook notifications for Help Center article lifecycle events. Five new topics are available:

- `article.created` — fires when an article is created
- `article.updated` — fires when an article's content or metadata is updated
- `article.published` — fires when an article is published
- `article.unpublished` — fires when an article is unpublished
- `article.deleted` — fires when an article is deleted


All topics require the **Read and list articles** permission. The `data.item` payload contains the full article object, including content, statistics, tags, and translated content (for multilingual help centers).

## Articles API: `parent_ids` replaces `parent_id` and `parent_type`

The `parent_id` and `parent_type` fields have been removed from article API **response** payloads.

The `parent_ids` field is now also **writable** on create and update. You can send an array of collection IDs to place an article in one or more collections, or send an empty array to remove it from all collections.

**Affected endpoints:**

- `GET /articles` — `parent_id` and `parent_type` removed from response
- `POST /articles` — `parent_ids` now accepted in request; `parent_id` and `parent_type` removed from response
- `GET /articles/{id}` — `parent_id` and `parent_type` removed from response
- `PUT /articles/{id}` — `parent_ids` now accepted in request; `parent_id` and `parent_type` removed from response
- `GET /articles/search` — `parent_id` and `parent_type` removed from response


## Breaking change: Contact owner_id now returns as string

The `Contact.owner_id` field now returns as a string type in the Preview API version, aligning with `Admin.id`. This change only affects the Preview version—all stable versions (2.15, 2.14, etc.) continue to return `owner_id` as an integer for backward compatibility.

**Affected endpoints:**

- `GET /contacts/{id}`
- `POST /contacts`
- `PUT /contacts/{id}`


**Example responses:**

*Before:*

```json
{
  "id": "5ba682d23d7cf92bef87bfd4",
  "owner_id": 321
}
```

*After:*

```json
{
  "id": "5ba682d23d7cf92bef87bfd4",
  "owner_id": "321"
}
```

## Identity Verification Secret rotation via API

You can now list, create, and delete Messenger [Identity Verification](https://www.intercom.com/help/en/articles/183-enable-identity-verification-for-web-and-mobile) secrets programmatically, without using the Intercom dashboard. This lets you script rotation into your existing secret-management workflows.

**New endpoints:**

- `GET /secure_mode_secrets` — list configured secrets (metadata only).
- `POST /secure_mode_secrets` — create a new 256-bit HMAC secret. The `secret` field is returned **only** in this response.
- `DELETE /secure_mode_secrets/{id}` — delete a secret (completes rotation out).


**Write-once semantics.** For safety, the raw signing material is only ever returned from `POST /secure_mode_secrets`. The list endpoint returns metadata only (name, platforms, `created_at`). If you lose the `secret` returned on create, you must rotate a new secret in and delete the lost one. This mirrors the write-once posture of AWS IAM access keys and GitHub fine-grained personal access tokens.

**Typical rotation flow:**

1. `POST /secure_mode_secrets` with a descriptive `name` and the platform(s) you want the secret enabled for — capture the returned `secret`.
2. Roll the new `secret` out to every client that signs `user_hash` values (your application backend, mobile apps, etc.). During this window, both the old and new secrets are valid, so end-user sessions remain uninterrupted.
3. Once all traffic is signing with the new secret, `DELETE /secure_mode_secrets/{old_id}` to finalize the rotation. Any sessions still signed with the old secret will fail verification on their next request.


The legacy `GET /secure_mode_secret` endpoint continues to work unchanged for backwards compatibility.

**OAuth scopes:** requires `write_secure_mode_secret` for create/delete and `read_secure_mode_secret` (or `read_all`) for list.

## Content Import Sources: Audience targeting

Content Import Sources now support audience targeting. You can associate audiences with a content import source so that imported content is scoped to specific user segments.

**New fields:**

- `audience_ids` (array of integers, nullable) — on create, update, and response payloads. Identifies the audiences associated with the content import source. Set to `null` or an empty array to remove all audiences.
- `apply_audience_to_existing_content` (boolean, default `false`) — on update requests only. When `true`, the audience is applied to all existing external pages belonging to the content import source.


**Affected endpoints:**

- `POST /ai/content_import_sources`
- `PUT /ai/content_import_sources/{id}`
- `GET /ai/content_import_sources`
- `GET /ai/content_import_sources/{id}`


## Breaking Change: Conversation assignee IDs return 0 instead of null when unassigned

The `admin_assignee_id` and `team_assignee_id` fields in Conversation API responses now return **`0`** instead of `null` when a conversation is unassigned.

**Before (null):**

```json
{
  "admin_assignee_id": null,
  "team_assignee_id": null
}
```

**After (integer):**

```json
{
  "admin_assignee_id": 0,
  "team_assignee_id": 0
}
```

## Breaking Change: Ticket assignee IDs are now integers

The `admin_assignee_id` and `team_assignee_id` fields in the Ticket API response are now returned as **integers** instead of strings. This aligns the Ticket API with the Conversation API, which already returns these fields as integers.

**Before (string):**

```json
{
  "admin_assignee_id": "991268013",
  "team_assignee_id": "0"
}
```

**After (integer):**

```json
{
  "admin_assignee_id": 991268013,
  "team_assignee_id": 0
}
```

## Custom Object Instances: list-all endpoint

`GET /custom_object_instances/{type}` now supports three modes:

- **No filter** — returns a paginated list of all instances for the type.
- **`references_contact_id`** — returns instances associated with the given contact.
- **`references_conversation_id`** — returns instances associated with the given conversation.


Supports `page` and `per_page` pagination parameters (max 150 per page).

## Sales Agent metadata on Conversations

Conversations now include `sales_agent_participated` and `sales_agent` fields, exposing metadata about Sales Agent (SDR) involvement. This mirrors the existing `ai_agent` object.

**New Fields on Conversation:**

- `sales_agent_participated` (boolean) — whether the Sales Agent participated
- `sales_agent` (object, nullable) — Sales Agent metadata, including:
  - `outcome` — fixed outcome enum (`qualified`, `disqualified`, `product_discovery`, `escalated_to_support`, `spam`)
  - `routing_outcome` — user-defined routing outcome identifier
  - `collected_data` — flat key-value map of memory fields collected during the conversation


**Applies to:**

- `GET /conversations/{id}`
- `GET /conversations/search`


## Side Conversations API

A new `GET /conversations/{id}/side_conversations` endpoint is now available for retrieving side conversations (internal teammate threads) associated with a conversation. Results are paginated and include the full conversation parts for each side conversation.

**New Endpoint:**

- [`GET /conversations/{id}/side_conversations`](https://developers.intercom.com/docs/references/unstable/rest-api/api.intercom.io/Conversations/listSideConversations)


**Query Parameters:**

- `page` (optional) - Page number of results to return (default: 1)
- `per_page` (optional) - Number of side conversations per page, between 1 and 50 (default: 25)


**Example Response:**

```json
{
  "type": "side_conversation.list",
  "side_conversations": [
    {
      "side_conversation_id": "456",
      "conversation_parts": [
        {
          "type": "conversation_part",
          "id": "789",
          "part_type": "comment",
          "body": "<p>Internal note about this issue</p>",
          "author": {
            "type": "admin",
            "id": "123",
            "name": "Jane Example",
            "email": "jane@example.com"
          }
        }
      ],
      "total_count": 1
    }
  ],
  "total_count": 1,
  "pages": {
    "type": "pages",
    "page": 1,
    "per_page": 25,
    "total_pages": 1
  }
}
```

**OAuth Scope:** `read_conversations`

## Company Activity Tracking via API

The `POST /companies` and `PUT /companies/{id}` endpoints now accept an `update_last_request_at` boolean parameter. When set to `true`, the company's `last_request_at` field is updated to the current time.

This is useful for workspaces that manage companies via the REST API and need to keep the "Last seen" value up to date. Requires the `write_users_companies` OAuth scope.

## Search Activity Logs API

A new `POST /admins/activity_logs/search` endpoint is now available for searching and filtering admin activity logs. This provides a dedicated search endpoint with event type filtering capabilities.

**New Endpoint:**

- [`POST /admins/activity_logs/search`](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Admins/searchActivityLogs)


**Request Body Parameters:**

- `created_at_after` (optional) - Start date as a UNIX timestamp (defaults to 30 days ago)
- `created_at_before` (optional) - End date as a UNIX timestamp
- `event_types` (optional) - Array of event type strings to filter by
- `page` (optional) - Page number of results to return (default: 1)
- `per_page` (optional) - Number of results per page, between 1 and 250 (default: 20)


**Example Request:**

```json
{
  "created_at_after": "1677253093",
  "created_at_before": "1677861493",
  "event_types": ["app_name_change", "message_state_change"]
}
```

Use the [`GET /admins/activity_log_event_types`](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Admins/listActivityLogEventTypes) endpoint to retrieve all valid event type values.

This endpoint requires the `read_admins` OAuth scope.

## Content Snippets API

Preview only
The Content Snippets API is available in the Preview API version only. It is not yet included in any stable API version.

New endpoints to manage content snippets via the public API. Content snippets are reusable pieces of content that power Fin (AI agent) and Copilot responses.

**Endpoints:**

- `GET /content_snippets` - List all content snippets (paginated)
- `GET /content_snippets/{id}` - Retrieve a single content snippet
- `POST /content_snippets` - Create a new content snippet
- `PUT /content_snippets/{id}` - Update an existing content snippet


**OAuth Scope:** `read_write_content_snippets`

**Example Request:**

```
GET /content_snippets
```

**Example Response:**

```json
{
  "type": "list",
  "data": [
    {
      "type": "content_snippet",
      "id": "123",
      "title": "How to reset your password",
      "locale": "en",
      "json_blocks": [
        { "type": "paragraph", "text": "Navigate to Settings > Security > Reset password." }
      ],
      "chatbot_availability": 1,
      "copilot_availability": 1,
      "created_at": 1663597223,
      "updated_at": 1663597223
    }
  ],
  "total_count": 1,
  "page": 1,
  "per_page": 50,
  "total_pages": 1
}
```

## Real-Time Team Metrics

A new `GET /teams/{team_id}/metrics` endpoint is now available, providing real-time per-admin conversation metrics (open, idle, snoozed counts) for a given team. This is designed for workforce management integrations that need to monitor agent workload in real time.

**Endpoint:** `GET /teams/{team_id}/metrics`

**Path Parameters:**

- `team_id` - The ID of the team to retrieve metrics for. Use `GET /teams` to list available team IDs.


**Optional Parameters:**

- `idle_threshold` - Seconds after which an open conversation is considered idle (default: 1800, clamped to 1–86400).


**Example Request:**

```
GET /teams/42/metrics
```

**Example Response:**

```json
{
  "type": "team_metric.list",
  "data": [
    {
      "type": "team_metric",
      "admin_id": "123",
      "open": 5,
      "idle": 2,
      "snoozed": 1
    }
  ]
}
```

This endpoint requires the real-time monitoring feature to be enabled for your workspace and the `read_admins` OAuth scope.

For complete documentation, see the [Team Metrics API Reference](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Teams/getTeamMetrics).

## Email History in Conversation Source

The `GET /conversations/{id}` endpoint now supports an `include_email_history` query parameter. When set to `true`, the `source` object of the conversation (email) will include an `email_message_metadata` field containing:

- `message_id` — the unique identifier from the email's `Message-ID` header
- `subject` — the subject of the email
- `email_address_headers` — a list of an email address headers
- `history` — the HTML content of any quoted or forwarded email history from the initial inbound message


**Updated Endpoint:**

- `GET /conversations/{id}?include_email_history=true` — returns `email_message_metadata` in the `source` object


**Note:** The `history` is only available for the conversation source (initial part), not for individual conversation parts. This parameter is not available on list/search endpoints.

## List Deleted Conversations API

You can now retrieve a list of deleted conversation IDs and their deletion timestamps using the new `GET /conversations/deleted` endpoint.

details
summary
Try Listing Deleted Conversations
## GET /fin_voice/conversation/{conversation_id} added

Added missing endpoint for retrieving Fin Voice calls associated with a conversation ID. Also backfilled to v2.15.

## Fin Agent API: New `replying` status for intermediate reply events

The `fin_replied` webhook event now uses a `replying` status for intermediate reply parts, and a separate `fin_status_updated` event with `awaiting_user_reply` fires as a "done" signal when Fin has finished replying. This replaces the previous behavior where every `fin_replied` event had `status: awaiting_user_reply`, making it impossible to distinguish intermediate replies from the final one.

**Before:**

```
fin_replied (status: awaiting_user_reply) — part 1
fin_replied (status: awaiting_user_reply) — part 2
fin_replied (status: awaiting_user_reply) — part 3
```

**After:**

```
fin_replied (status: replying) — part 1
fin_replied (status: replying) — part 2
fin_replied (status: replying) — part 3
fin_status_updated (status: awaiting_user_reply) — done signal
```

**What changed:**

- `fin_replied` event: `status` enum now includes `replying` (new default for intermediate replies) and `awaiting_user_reply` (legacy)
- `fin_status_updated` event: `status` enum now includes `awaiting_user_reply` in addition to `escalated`, `resolved`, and `complete`


Clients should update their integrations to listen for the `fin_status_updated` event with `status: awaiting_user_reply` as the signal that Fin is done replying and the user can respond.

## Activity Log Event Types API

A new endpoint lets you retrieve all possible event type values for admin activity logs. Use these values to understand what actions are trackable and to filter results when querying the [Activity Logs API](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Admins/listActivityLogs).

**New Endpoint:**

- `GET /admins/activity_log_event_types` - List all activity log event types


**Example Response:**

```json
{
  "type": "activity_log_event_type.list",
  "event_types": [
    "admin_login_success",
    "admin_logout",
    "app_name_change",
    "message_state_change"
  ]
}
```

**OAuth Scope Required:** `Read admins and teammates`

For complete documentation, see the [Activity Log Event Types API Reference](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Admins/listActivityLogEventTypes).

## Conversations API: `retain_metrics` parameter for conversation deletion

The [Delete a conversation](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Conversations/deleteConversation) endpoint now supports a `retain_metrics` query parameter that controls how conversation data is deleted:

- **`retain_metrics=true` (default)**: Deletes the conversation while retaining reporting data.
- **`retain_metrics=false`**: Deletes the conversation and all associated reporting data.


Required scope for retain_metrics=false
Using `retain_metrics=false` requires the `delete_conversations_and_metrics` OAuth scope. See [OAuth Scopes](/docs/build-an-integration/learn-more/authentication/oauth-scopes) for details.

**Example Request:**

```bash
curl -X DELETE https://api.intercom.io/conversations/123?retain_metrics=false \
  -H 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
  -H 'Accept: application/json' \
  -H 'Intercom-Version: Preview'
```

For more info, see [this help center article](https://www.intercom.com/help/en/articles/13885146-deleting-a-conversation).

## Folder Management API

A new top-level Folders API is now available at `/folders` for organizing content into hierarchical folder structures.

**New endpoints:**

- `GET /folders` — List all folders (supports pagination and `parent_folder_id` filter)
- `POST /folders` — Create a new folder
- `GET /folders/{id}` — Retrieve a folder
- `PUT /folders/{id}` — Update a folder
- `DELETE /folders/{id}` — Delete a folder (must be empty)


**Article and Internal Article changes:**

- A `folder_id` field has been added to article and internal article responses, as well as create and update request payloads, allowing you to organize articles into folders.
- The `/internal_articles/search` endpoint's `folder_id` parameter now searches recursively through descendant folders.


## Ticket state change tracking

The Ticket object now includes a `previous_ticket_state_id` field that returns the ID of the ticket's previous state. This enables tracking ticket state transitions for reporting and compliance. The field returns `null` when no state change history exists.

## Company Notes API

You can now create notes on company records via the API using `POST /companies/{company_id}/notes`, mirroring the existing contact notes functionality. The request and response format is identical to contact notes, with an optional `admin_id` parameter that defaults to the token owner. Notes are polymorphic — a single note belongs to either a contact or a company, never both.

## Admin role now included in API responses

The admin object returned by `GET /admins`, `GET /admins/{id}`, `GET /me`, and `PUT /admins/{id}/away` now includes a `role` attribute when the admin has a role assigned. The `role` object contains the role `id` and `name`.

**Example:**

```json
{
  "type": "admin",
  "id": "1295",
  "name": "Joe Example",
  "role": {
    "type": "role",
    "id": "456",
    "name": "Support Agent"
  }
}
```

**Note:** Admins with custom per-user permissions (no role assigned) will not have the `role` field.

## Conversations API: display_as parameter behavior

The `display_as` query parameter in the Retrieve Conversation and List Conversation Parts endpoints now affects both the `body` and `subject` fields of conversation messages when set to `plaintext`. By default, both fields are returned in HTML format.

## Conversation Webhook Serialization Alignment

Conversation webhook payloads now use the same serialization format as API responses:

- **Statistics date fields** (`first_contact_reply_at`, `first_assignment_at`, `first_admin_reply_at`, `first_close_at`, `last_assignment_at`, `last_assignment_admin_reply_at`, `last_contact_reply_at`, `last_admin_reply_at`, `last_close_at`) now return Unix timestamp integers instead of ISO8601 strings.
- **`body` and `subject` fields** now return plain text instead of HTML.


## Data Connectors API

We've added a new Data Connectors API that lets you create, update, delete, and list your data connectors, plus view their execution history. Data connectors allow you to make HTTP requests to external APIs from Intercom workflows and AI agents.

**Endpoints:**

- `POST /data_connectors` - Create a new data connector
- `PATCH /data_connectors/{id}` - Update a data connector (fields, state transitions)
- `DELETE /data_connectors/{id}` - Delete a data connector
- `GET /data_connectors` - List all data connectors (paginated)
- `GET /data_connectors/{id}` - Retrieve a single data connector with full detail
- `GET /data_connectors/{id}/execution_results` - List execution results
- `GET /data_connectors/{id}/execution_results/{id}` - Retrieve a single execution result


**Key Features:**

- **Create in draft**: New connectors start in `draft` state — configure URL, headers, data inputs, and audiences before publishing
- **Audience targeting**: Control which user types (leads, users, visitors) can trigger the connector
- **Safe deletion**: Returns `409` if the connector is in use or not in draft state
- **Cursor-based pagination**: Navigate through data connectors and execution results using `starting_after` and `per_page`
- **Filtering**: Filter execution results by `success`, `error_type`, and time range
- **Opt-in bodies**: Use `include_bodies=true` to include request/response bodies in execution results


**OAuth Scope Required:** `Read data connectors` (read-only) or `Read and write data connectors` (create/update/delete)

For complete documentation and examples, see the [Data Connectors API Reference](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Data-Connectors).

## New Data Connector execution webhook topic

You can now receive real-time notifications when Data Connector actions complete execution using the new `data_connector.execution.completed` webhook topic. This webhook provides operational visibility into connector execution outcomes, including success/failure status, HTTP response codes, error categorization, and execution timing. See the [webhook models](/docs/references/preview/webhooks/webhook-models) for details on how to subscribe to execution events.

## Macros API

We've added a new Macros API that provides programmatic access to saved replies (macros) in Intercom. This API enables external applications to retrieve and display your team's pre-written responses, making it easier to integrate Intercom macros with third-party tools and workflows.

**New Endpoints:**

- `GET /macros` - List all available macros with pagination
- `GET /macros/{id}` - Retrieve a specific macro by ID


**Key Features:**

- **Cursor-based pagination**: Navigate through large sets of macros efficiently using the `starting_after` parameter with Base64-encoded cursors
- **Smart placeholder transformation**: Macros containing Intercom placeholders (e.g., `{{user.name}}`) are automatically transformed to XML-like attributes (`<attribute key="user.name"/>`) for easier parsing
- **Flexible filtering**: Use the `updated_since` parameter to retrieve only recently modified macros
- **Team visibility controls**: Respects macro visibility settings, showing only macros available to the authenticated user's teams
- **Channel availability**: Each macro indicates where it can be used (`inbox`, `messenger`, or both)


**OAuth Scope Required:** `READ_CONVERSATIONS`

**Use Cases:**

- Integration with training platforms like Solidroad for customer service simulations
- Building custom macro management interfaces
- Exporting macros for documentation or backup purposes
- Analyzing macro usage and content across your organization


**Example Request:**

```bash
curl -X GET https://api.intercom.io/macros?per_page=10 \
  -H 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
  -H 'Accept: application/json' \
  -H 'Intercom-Version: Preview'
```

**Example Response:**

```json
{
  "type": "list",
  "data": [
    {
      "type": "macro",
      "id": "123",
      "name": "Order Status Update",
      "body": "<p>Hi <attribute key=\"user.name\" default=\"there\"/>, your order is ready!</p>",
      "body_text": "Hi there, your order is ready!",
      "created_at": "2025-07-17T11:18:08.000Z",
      "updated_at": "2025-07-17T15:30:24.000Z",
      "visible_to": "everyone",
      "visible_to_team_ids": [],
      "available_on": ["inbox", "messenger"]
    }
  ],
  "pages": {
    "type": "pages",
    "per_page": 10,
    "next": {
      "starting_after": "WzE3MTk0OTM3NTcuMCwgIjEyMyJd"
    }
  }
}
```

**Important Notes:**

- This API is currently available in the Preview version only
- Placeholders in macro bodies are transformed from Intercom format to XML-like attributes
- The pagination cursor encodes `[updated_at, id]` for stable ordering
- Maximum 150 macros can be retrieved per request


For complete documentation and more examples, see the [Macros API Reference](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/macros/).

## New WhatsApp Message status API

The [WhatsApp Message status API](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/whatsapp/getwhatsappmessagestatus) is now available in the Preview version. This endpoint returns paginated status events for WhatsApp messages sent via the Outbound module, providing information about delivery state and related message details.

## Enforcement of ticket permissions on the Conversations API

Starting in early August 2024, accessing a ticket directly through the [Conversations API](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Conversations/retrieveConversation/) will requires the "Read tickets" or "Write tickets" scope, as appropriate. When [listing conversations](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Conversations/listConversations/) or [searching conversations](https://developers.intercom.com/docs/references/preview/rest-api/api.intercom.io/Conversations/searchConversations/), tickets will only be returned in the results if the caller has the "Read tickets" scope.

Breaking permission changes
Previously, tickets were accessible through the Conversations API using only the "Read conversations" or "Write conversations" scope(s). Any apps relying on this behaviour will need to add the appropriate ticket permission, and have users reauthenticate.

## New Searchable Contact Attribute

We added `formatted_phone` as a searchable attribute for contacts. This is the contact's phone number normalized to the E164 format.

## Reporting Data Export API: qualified_id for Attribute Disambiguation

The Reporting Data Export API now includes a `qualified_id` field in the `get_datasets` response to uniquely identify attributes across different types.

**Problem Solved:**
Custom attributes can have the same name across different types (e.g., a "Brand" attribute might exist for both user custom data and conversation custom data). Previously, using just the `id` field could result in ambiguous attribute selection.

**New Field:**
The `qualified_id` field uses a namespaced format: `"type.name"` (e.g., `"people.Brand"`, `"conversation.Brand"`).

**Available Prefixes:**

- `people` - User custom data attributes
- `company` - Company custom data attributes
- `conversation` - Conversation custom data attributes
- `ticket` - Ticket attributes
- `system` - System-defined attributes


**Example Response:**

```json
{
  "type": "list",
  "data": [
    {
      "id": "conversation",
      "name": "Conversation",
      "attributes": [
        {
          "id": "Brand",
          "qualified_id": "people.Brand",
          "name": "Brand"
        }
      ]
    }
  ]
}
```

**Usage:**
When calling the `enqueue` endpoint, you must use `qualified_id` values in the `attribute_ids` array. The simple `id` is no longer accepted — use the `qualified_id` from the `get_datasets` response to ensure unambiguous attribute selection.