Skip to content

Changelog (2.18)

For changes that have been updated across all versions, see the Unversioned changes page.

There are breaking changes in this version, which are detailed below.

Breaking Changes

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.

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, GET /conversations/{id} and POST /conversations/search. API version 2.17 and earlier still return both fields.

Breaking change: Conversations: line breaks preserved in plaintext

When a conversation body or conversation part body is returned as plain text, a line break inside a paragraph now renders as a newline (\n) instead of a space. Multi-line notes, such as an AI-generated conversation summary, keep the line-by-line layout shown in the Inbox. Earlier API versions are unchanged.

Affected endpoints:

  • GET /conversations/{id} (with display_as=plaintext)
  • GET /conversations (with display_as=plaintext)
  • Conversation webhook topics, for example conversation.admin.noted and conversation.admin.replied

Breaking change: Reporting Data Export ticket field qualified_id includes the ticket type

The GET /export/reporting_data/get_datasets endpoint now returns a ticket field's qualified_id with its ticket type. For example, a "Priority" field on the "Bug" ticket type is now ticket.Bug.Priority instead of ticket.Priority.

The POST /export/reporting_data/enqueue endpoint accepts only the new format for ticket fields. A ticket field id without its ticket type returns a 400 error. CSV column headers do not change.

Validation error codes are always strings

The code field on an error object is now always a string. Previously, a few request validation errors (for example a malformed company data structure) returned an HTTP status code as an integer here instead, such as 422. All error codes now use the same string format.

Audience and dynamic content names no longer double-escape

Audience and dynamic content names are HTML-unescaped when written, as company names already are, so reading a name and sending it back no longer double-escapes it.

Changes

Contacts: more fields on bulk update

The PUT /contacts/bulk endpoint now accepts these fields on each contact, with the same rules as PUT /contacts/{id}:

  • role
  • signed_up_at
  • last_seen_at
  • sms_consent
  • unsubscribed_from_sms

Relationship attributes in custom_attributes now also link the contact to the custom object records you send.

A contact with an invalid value fails on its own. The rest of the job is still applied, and its state ends as completed_with_errors.

Help Center Redirects: redirect to the home page

A redirect can now send visitors to a help center's home page, not just to an article or collection. Set target_type to help_center when you create a redirect. The locale picks which language of the home page visitors land on.

target_id is optional for these redirects and defaults to the help center in the request path. If you send it, it must match that help center, or the request responds 422. The list and retrieve endpoints return home page redirects with target_type set to help_center and target_id set to the help center's id.

Articles: empty paragraphs preserved

When you create or update an article, empty <p> tags in body are now kept as blank paragraphs instead of being dropped.

Contacts: user-to-user merge support

POST /contacts/merge 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.

New Features

Filter conversation parts by type

Retrieving a conversation 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.

Page through a conversation's parts

The GET /conversations/{id} endpoint now accepts two query parameters:

  • page (integer): When sent, the conversation's parts are returned a page at a time, starting from the oldest part. Page 1 holds the oldest parts, and each page lists its parts oldest first.
  • per_page (integer): The number of parts per page, from 1 to 500. Defaults to 20. It is ignored unless page is sent.

When page is sent, conversation_parts includes a pages object with type, page, per_page, total_pages and next. next is the URL of the next page, and is null on the last page. total_count is the number of parts on the current page.

Part type and created-at filters are applied before paging. An invalid page or per_page returns a 400. Requests without page are unchanged and still return the 500 most recent parts.

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 — List every role in the workspace.
  • GET /roles/{id} — Retrieve a single role. Responds 404 if no role with that id exists in your workspace.

An access token needs 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.

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} call to find out which email address a conversation is with. email is null for a contact with no email on record.

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.
"event_details": {
  "handoff": {
    "type": "salesforce_case",
    "success": true,
    "data": { "case_id": "500XX0000001", "case_number": "00001234" }
  }
}

Ecommerce catalog upload

The POST /ecommerce/connectors/{id}/catalog 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:

curl -X POST "https://api.intercom.io/ecommerce/connectors/{id}/catalog" \
  -H "Authorization: Bearer {access_token}" \
  -H "Intercom-Version: 2.18" \
  -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.

Ecommerce product events

The POST /ecommerce/connectors/{id}/events endpoint is now available. Use it to send one product change at a time, so Fin's product knowledge stays current between full catalog uploads. Upload a full catalog first — until you do, this endpoint returns 409 with type catalog_not_uploaded.

Every event needs a type and an occurred_at timestamp (ISO 8601). There are three event types:

  • product.created and product.updated — send the full product object in data.product, with the same fields as a catalog product. id, title, url and updated_at are required.
  • product.deleted — send only data.product_id.

The endpoint returns 202 with an event_id once the event is queued. Processing is asynchronous.

Conversations: end-user IP

Conversation parts and the conversation source now include user_ip, the IP address of the end user who sent the part or opened the conversation. It is null when the IP address isn't available.

Conversation parts: delivery receipts

Conversation parts can now include receipts, describing what happened to the part for each recipient: each recipient's own delivery state, the reason when Intercom could not send to them, and an overall_state for the part. A part reports partially_sent when some of its recipients have it and others do not.

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 does not include grants.

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.

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.

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 — List the workspace's stored credentials. Paginated with per_page (default 20, maximum 50) and starting_after.
  • GET /auth_tokens/{id} — 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.

Create and update

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

  • POST /auth_tokens — Store a new credential. name, token_value and request_header_key are required; token_prefix and description are optional.
  • PATCH /auth_tokens/{id} — Update a credential's name, request_header_key, token_prefix or description. 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 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.

POST /auth_tokens and PATCH /auth_tokens/{id} 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.

Create an OAuth credential

POST /auth_tokens also 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.

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:

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:

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 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 read_conversations scope. Responds 404 if the conversation does not exist, and 403 if the teammate cannot access it.

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 for payload details, and Set up Webhooks to configure a subscription.