fikadesk cloud is in private alpha. Cells are provisioned by hand and the product is not ready for real customers yet.

Request early access

August 31, 20264 min read

Leaving Intercom without losing your history

A walkthrough of the Intercom importer: the token, the preview, what maps to what, and the parts we tell you we left behind.

By fikadesk

Moving support tools usually means starting the conversation history over. Intercom gives you an access token and a REST API, so we wrote an importer that pulls contacts, companies, conversations and tags into fikadesk, and tells you what it could not bring. This is that importer, from token to finished run.

You start in the inbox settings, under Import. Pick Intercom, paste the access token from your Intercom workspace, and hit Preview counts. The preview does four requests, one per collection, each asking for per_page=1 so the total_pages field is the exact count.

Preview
GET /contacts?per_page=1
GET /companies?per_page=1
GET /conversations?per_page=1
GET /tags?per_page=1

You see four numbers before anything is written. A refused token comes back as invalid_token, not a stack trace, and the token is dropped after the preview. It is never stored.

When you start the run, the token is sealed with AES-256-GCM under FIKADESK_IMPORT_SECRET, a 32-byte key you generate at deploy time, before the run row exists. The row's config jsonb holds only the id of the member who started the import. The plaintext token never touches the database. Leave FIKADESK_IMPORT_SECRET unset and hosted imports are off entirely; the CSV importer still works.

The token is also never echoed back. The wizard shows only the preview counts, and the run row stores the sealed ciphertext, so your Intercom credential is not rendered anywhere in the product after you paste it. If you revoke the token in Intercom, the next run refuses it with invalid_token.

What maps to what

Contacts bring email, phone, name, their Intercom id as external_id, and custom attributes. A scalar attribute keeps its type; anything nested is stringified so the jsonb column holds one value type per key. Locale, timezone and country are not mapped from Intercom today. Companies bring name, their Intercom id, plan, monthly spend, size, industry, website and custom attributes. Tags are created by name, deduplicated case-insensitively, and attached to contacts and conversations.

Conversations are pulled by scrolling the list, then fetching each one with display_as=plaintext. The title becomes both title and subject. A closed conversation imports as closed; everything else imports as open, so a snoozed Intercom conversation lands as open. The original created_at and updated_at are kept. Parts map only when they are comments or notes, and only when the body is not empty. A contact author becomes the contact, a bot author becomes a system message, and any admin author is attributed to the member who started the import.

A re-run is safe. Every entity keys on its Intercom id as external_id, so a second run upserts contacts, companies and conversations instead of duplicating them. If a run crashes and you start it again, the history does not double.

Imported conversations are written directly, not through the normal conversation service. That keeps the original timestamps and stops conversation.created and part events from firing. Emitting them would run your automation rules and push every historical thread into the live inbox as if it had just arrived.

What does not import, and why we say so

Attachments do not import. The importer reads each part's body text, not the attachments list Intercom returns beside it. Per-admin authorship does not import either: every admin-written message is attributed to the importing member, and the run summary tells you how many source admins were folded into you that way.

Intercom's other part types, the ones that record assignment, open and close events rather than a message, are dropped. They are state changes. A conversation with more than 500 parts imports the first 500 and records the truncation in the summary. A conversation deleted between the list and the per-conversation fetch is recorded as a row error that it no longer exists, and the run moves on.

Every one of these shows up in the finished run. The summary says how many Intercom admins were attributed to you, and how many conversations had more than 500 parts. Row errors list the conversations that vanished mid-run. We tell you what we dropped because a history import you cannot audit is worse than one you have to do again.

Resumable by construction

The scroll keeps a cursor per collection, so a page is one checkpoint. A crash leaves the run marked running with the cursors persisted, and a redelivered job picks the scroll up from the next page instead of from zero. Pages are 150 items. Rate limits are respected: a 429 or a 5xx backs off using Retry-After or X-RateLimit-Reset, with a 250 ms floor and a doubling backoff, and gives up after five attempts. Every request sets Intercom-Version: 2.16.

The client talks to api.intercom.io. A workspace on Intercom's EU or Australia region is a base-url change, not a code change, so the same scroll and retry logic runs against either host.

The result is a contact list and an inbox you can work from, with the timestamps and the thread structure intact. It is not a byte-for-byte mirror of Intercom. Attachments and per-admin attribution are the two things we would rather tell you we left behind than pretend we copied.