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-clientSDK 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. Stampmigrated_from+migration_datecustom 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 whereerrors[]carries per-contact{ contact_id, email, error }for every failure. - Orchestrator (
executeMigration) prints a per-phase progress log and a finalMigration complete in N minutesline 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 anOK/FAILline 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[], runimportContacts()(Step 1), then reconcile against the returnederrors[]. Full function: references/implementation.md. - Full re-platform with history — build a
MigrationPlan(contacts, companies, tags, articles) and runexecuteMigration(plan)(Step 5), thenvalidateMigration(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
- Full implementation walkthrough — all six phases, rollback, and validation in runnable TypeScript
- Contacts API
- Companies API
- Articles API
- Import Contacts Guide
- Tags API