Intercom Migration Deep Dive
Overview
Comprehensive guide for migrating to Intercom from other platforms (Zendesk,
Freshdesk, HelpScout) or bulk-importing data. Covers contact import, company
import, tags, Help Center articles, orchestration, and post-migration
validation. The full runnable TypeScript for every phase lives in
references/implementation.md; this file carries
the workflow and the first-phase skeleton so you can follow it end to end, then
drill into the reference for depth.
Prerequisites
- Intercom workspace with an access token exported as
INTERCOM_ACCESS_TOKEN
- Source system data exported (CSV or API access)
- The
intercom-client SDK installed (npm install intercom-client)
- Feature flag infrastructure for gradual cutover
- Rollback strategy tested
Authentication
All scripts read the workspace access token from the environment — never
hard-code it. Create the token in the Intercom Developer Hub (Settings →
Developers → your app → Authentication), then:
export INTERCOM_ACCESS_TOKEN="your-workspace-access-token"
import { IntercomClient, IntercomError } from "intercom-client";
const client = new IntercomClient({ token: process.env.INTERCOM_ACCESS_TOKEN! });
Migration Types
| Type |
Complexity |
Duration |
Risk |
| Contact import |
Low |
Hours |
Low |
| Zendesk/Freshdesk migration |
Medium |
1-2 weeks |
Medium |
| Full re-platform (with history) |
High |
2-4 weeks |
High |
| Help Center migration |
Medium |
Days |
Low |
Instructions
Run the phases in dependency order. Each phase is a standalone function in
references/implementation.md; the orchestrator in
Step 5 chains them.
- Contacts (Step 1) — idempotent: search by
external_id/email, then
update or create. Stamp migrated_from + migration_date custom attributes
so rollback can find migrated records. Skeleton below.
- Companies (Step 2) — import before attaching contacts; contacts reference
companies.
- Tags (Step 3) — create each tag, apply to its contacts, skip missing
(404) contacts instead of aborting.
- Articles (Step 4) — group into Help Center collections by category,
creating each collection once.
- Orchestrate (Step 5) —
executeMigration(plan) runs companies → contacts
→ tags → articles with per-phase progress logging.
- Validate (Step 6) —
validateMigration(expectedCounts) compares live
counts against source counts (95% threshold for contacts/articles).
Contact-import skeleton (full body in the reference):
async function importContacts(contacts: SourceContact[]) {
const stats = { created: 0, updated: 0, failed: 0, errors: [] as any[] };
for (const contact of contacts) {
const existing = await client.contacts.search({
query: { operator: "OR", value: [
{ field: "external_id", operator: "=", value: contact.id },
{ field: "email", operator: "=", value: contact.email },
] },
});
if (existing.data.length > 0) {
await client.contacts.update({ contactId: existing.data[0].id, /* ...attrs */ });
stats.updated++;
} else {
await client.contacts.create({ role: "user", externalId: contact.id, /* ...attrs */ });
stats.created++;
}
}
return stats;
}
See references/implementation.md for the complete
error handling, rate limiting, company/tag/article functions, orchestrator, and
validation code.
Output
- Contact import returns
{ created, updated, failed, errors[] } — a
reconciliation record where errors[] carries per-contact { contact_id, email, error } for every failure.
- Orchestrator (
executeMigration) prints a per-phase progress log and a
final Migration complete in N minutes line plus the first 10 failed contacts.
- Validation (
validateMigration) returns { passed, checks[] } where each
check is { name, expected, actual, passed }, and prints a PASSED/FAILED
summary with an OK/FAIL line per resource.
Error Handling
| Issue |
Cause |
Solution |
| 409 Conflict |
Duplicate external_id/email |
Search before create |
| 429 Rate Limited |
Too fast |
Add delays between batches |
| 422 Validation |
Bad email/data format |
Validate data before import |
| Partial migration |
Script crashed |
Use idempotent operations, re-run |
| Missing conversations |
API doesn't support bulk import |
Contact Intercom support for import |
Rollback: keep the source system active during migration; only decommission
after validation plus a 2-week parallel run. To reverse, search by
custom_attributes.migration_date and delete migrated contacts in batches — see
the Rollback Procedure in
references/implementation.md.
Examples
- Bulk contact import from Zendesk — export contacts to
SourceContact[],
run importContacts() (Step 1), then reconcile against the returned
errors[]. Full function: references/implementation.md.
- Full re-platform with history — build a
MigrationPlan (contacts,
companies, tags, articles) and run executeMigration(plan) (Step 5), then
validateMigration(expectedCounts) (Step 6). Full orchestrator +
validation: references/implementation.md.
- Help Center article migration — map categories to collections and run
migrateArticles(articles, authorId) (Step 4):
references/implementation.md.
Resources
1---2name: intercom-migration-deep-dive3description: Use when migrating from Zendesk/Freshdesk/HelpScout to Intercom, bulk-importing contacts, or re-platforming to Intercom with the contacts, conversations, and articles APIs. Trigger with phrases like "migrate to intercom", "intercom migration", "import contacts to intercom", "switch to intercom", "zendesk to intercom", "intercom data import".4license: MIT5---6# Intercom Migration Deep Dive
7
8## Overview
9
10Comprehensive guide for migrating to Intercom from other platforms (Zendesk,
11Freshdesk, HelpScout) or bulk-importing data. Covers contact import, company
12import, tags, Help Center articles, orchestration, and post-migration
13validation. The full runnable TypeScript for every phase lives in
14[references/implementation.md](references/implementation.md); this file carries
15the workflow and the first-phase skeleton so you can follow it end to end, then
16drill into the reference for depth.
17
18## Prerequisites
19
20- Intercom workspace with an access token exported as `INTERCOM_ACCESS_TOKEN`
21- Source system data exported (CSV or API access)
22- The `intercom-client` SDK installed (`npm install intercom-client`)
23- Feature flag infrastructure for gradual cutover
24- Rollback strategy tested
25
26## Authentication
27
28All scripts read the workspace access token from the environment — never
29hard-code it. Create the token in the Intercom Developer Hub (Settings →
30Developers → your app → Authentication), then:
31
32```bash
33export INTERCOM_ACCESS_TOKEN="your-workspace-access-token"
34```
35
36```typescript
37import { IntercomClient, IntercomError } from "intercom-client";
38const client = new IntercomClient({ token: process.env.INTERCOM_ACCESS_TOKEN! });
39```
40
41## Migration Types
42
43| Type | Complexity | Duration | Risk |
44|------|-----------|----------|------|
45| Contact import | Low | Hours | Low |
46| Zendesk/Freshdesk migration | Medium | 1-2 weeks | Medium |
47| Full re-platform (with history) | High | 2-4 weeks | High |
48| Help Center migration | Medium | Days | Low |
49
50## Instructions
51
52Run the phases in dependency order. Each phase is a standalone function in
53[references/implementation.md](references/implementation.md); the orchestrator in
54Step 5 chains them.
55
561. **Contacts** (Step 1) — idempotent: search by `external_id`/`email`, then
57 update or create. Stamp `migrated_from` + `migration_date` custom attributes
58 so rollback can find migrated records. Skeleton below.
592. **Companies** (Step 2) — import before attaching contacts; contacts reference
60 companies.
613. **Tags** (Step 3) — create each tag, apply to its contacts, skip missing
62 (404) contacts instead of aborting.
634. **Articles** (Step 4) — group into Help Center collections by category,
64 creating each collection once.
655. **Orchestrate** (Step 5) — `executeMigration(plan)` runs companies → contacts
66 → tags → articles with per-phase progress logging.
676. **Validate** (Step 6) — `validateMigration(expectedCounts)` compares live
68 counts against source counts (95% threshold for contacts/articles).
69
70Contact-import skeleton (full body in the reference):
71
72```typescript
73async function importContacts(contacts: SourceContact[]) {
74 const stats = { created: 0, updated: 0, failed: 0, errors: [] as any[] };
75 for (const contact of contacts) {
76 const existing = await client.contacts.search({
77 query: { operator: "OR", value: [
78 { field: "external_id", operator: "=", value: contact.id },
79 { field: "email", operator: "=", value: contact.email },
80 ] },
81 });
82 if (existing.data.length > 0) {
83 await client.contacts.update({ contactId: existing.data[0].id, /* ...attrs */ });
84 stats.updated++;
85 } else {
86 await client.contacts.create({ role: "user", externalId: contact.id, /* ...attrs */ });
87 stats.created++;
88 }
89 }
90 return stats;
91}
92```
93
94See [references/implementation.md](references/implementation.md) for the complete
95error handling, rate limiting, company/tag/article functions, orchestrator, and
96validation code.
97
98## Output
99
100- **Contact import** returns `{ created, updated, failed, errors[] }` — a
101 reconciliation record where `errors[]` carries per-contact `{ contact_id,
102 email, error }` for every failure.
103- **Orchestrator** (`executeMigration`) prints a per-phase progress log and a
104 final `Migration complete in N minutes` line plus the first 10 failed contacts.
105- **Validation** (`validateMigration`) returns `{ passed, checks[] }` where each
106 check is `{ name, expected, actual, passed }`, and prints a PASSED/FAILED
107 summary with an `OK`/`FAIL` line per resource.
108
109## Error Handling
110
111| Issue | Cause | Solution |
112|-------|-------|----------|
113| 409 Conflict | Duplicate external_id/email | Search before create |
114| 429 Rate Limited | Too fast | Add delays between batches |
115| 422 Validation | Bad email/data format | Validate data before import |
116| Partial migration | Script crashed | Use idempotent operations, re-run |
117| Missing conversations | API doesn't support bulk import | Contact Intercom support for import |
118
119**Rollback:** keep the source system active during migration; only decommission
120after validation plus a 2-week parallel run. To reverse, search by
121`custom_attributes.migration_date` and delete migrated contacts in batches — see
122the Rollback Procedure in
123[references/implementation.md](references/implementation.md).
124
125## Examples
126
127- **Bulk contact import from Zendesk** — export contacts to `SourceContact[]`,
128 run `importContacts()` (Step 1), then reconcile against the returned
129 `errors[]`. Full function: [references/implementation.md](references/implementation.md).
130- **Full re-platform with history** — build a `MigrationPlan` (contacts,
131 companies, tags, articles) and run `executeMigration(plan)` (Step 5), then
132 `validateMigration(expectedCounts)` (Step 6). Full orchestrator +
133 validation: [references/implementation.md](references/implementation.md).
134- **Help Center article migration** — map categories to collections and run
135 `migrateArticles(articles, authorId)` (Step 4):
136 [references/implementation.md](references/implementation.md).
137
138## Resources
139
140- [Full implementation walkthrough](references/implementation.md) — all six
141 phases, rollback, and validation in runnable TypeScript
142- [Contacts API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts)
143- [Companies API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/companies)
144- [Articles API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/articles)
145- [Import Contacts Guide](https://developers.intercom.com/docs/guides/tickets/import-contacts)
146- [Tags API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/tags)