--- name: intercom-migration-deep-dive description: 'Execute major Intercom data migrations and re-platforming with the contacts, conversations, and articles APIs. Use when migrating from Zendesk/Freshdesk to Intercom, bulk-importing contacts, or re-platforming to Intercom. Trigger with phrases like "migrate to intercom", "intercom migration", "import contacts to intercom", "switch to intercom", "zendesk to intercom", "intercom data import". ' allowed-tools: Read, Write, Edit, Bash(npm:*), Bash(node:*) version: 1.0.0 license: MIT author: Jeremy Longshore tags: - saas - support - messaging - intercom compatibility: Designed for Claude Code --- # Intercom Migration Deep Dive ## Overview Comprehensive guide for migrating to Intercom from other platforms (Zendesk, Freshdesk, HelpScout) or bulk-importing data. Covers contact import, conversation history, Help Center articles, tags, and companies. ## Prerequisites - Intercom workspace with access token - Source system data exported (CSV or API access) - Feature flag infrastructure for gradual cutover - Rollback strategy tested ## 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 ### Step 1: Contact Import ```typescript import { IntercomClient, IntercomError } from "intercom-client"; const client = new IntercomClient({ token: process.env.INTERCOM_ACCESS_TOKEN!, }); interface SourceContact { id: string; email: string; name: string; phone?: string; plan?: string; company?: string; created_at: string; custom_fields?: Record; } async function importContacts( contacts: SourceContact[] ): Promise<{ created: number; updated: number; failed: number; errors: any[] }> { const stats = { created: 0, updated: 0, failed: 0, errors: [] as any[] }; for (const contact of contacts) { try { // Search for existing contact by external_id or email 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) { // Update existing contact await client.contacts.update({ contactId: existing.data[0].id, name: contact.name, phone: contact.phone, customAttributes: { ...contact.custom_fields, plan: contact.plan, migrated_from: "source_system", migration_date: new Date().toISOString(), }, }); stats.updated++; } else { // Create new contact await client.contacts.create({ role: "user", externalId: contact.id, email: contact.email, name: contact.name, phone: contact.phone, signedUpAt: Math.floor(new Date(contact.created_at).getTime() / 1000), customAttributes: { ...contact.custom_fields, plan: contact.plan, migrated_from: "source_system", migration_date: new Date().toISOString(), }, }); stats.created++; } // Rate limit: pause every 50 contacts if ((stats.created + stats.updated) % 50 === 0) { console.log(`Progress: ${stats.created} created, ${stats.updated} updated`); await new Promise(r => setTimeout(r, 500)); } } catch (err) { stats.failed++; stats.errors.push({ contact_id: contact.id, email: contact.email, error: err instanceof IntercomError ? `${err.statusCode}: ${err.message}` : (err as Error).message, }); } } return stats; } ``` ### Step 2: Company Import ```typescript async function importCompanies( companies: Array<{ id: string; name: string; plan?: string; size?: number }> ): Promise { for (const company of companies) { await client.companies.create({ companyId: company.id, name: company.name, plan: company.plan, size: company.size, customAttributes: { migrated_from: "source_system", }, }); await new Promise(r => setTimeout(r, 100)); // Rate limit } } // Attach contacts to companies async function attachContactToCompany( contactId: string, companyId: string ): Promise { await client.contacts.attachCompany({ contactId, companyId, }); } ``` ### Step 3: Tag Migration ```typescript async function migrateTags( tagMappings: Array<{ sourceName: string; contactIds: string[] }> ): Promise { for (const mapping of tagMappings) { // Create tag if it doesn't exist const tag = await client.tags.create({ name: mapping.sourceName }); // Apply tag to contacts for (const contactId of mapping.contactIds) { try { await client.contacts.tag({ contactId, id: tag.id }); } catch (err) { if (err instanceof IntercomError && err.statusCode === 404) { console.warn(`Contact ${contactId} not found, skipping tag`); continue; } throw err; } } console.log(`Tagged ${mapping.contactIds.length} contacts with "${mapping.sourceName}"`); } } ``` ### Step 4: Help Center Article Migration ```typescript async function migrateArticles( articles: Array<{ title: string; body: string; // HTML content category: string; state: "published" | "draft"; }>, authorId: string // Admin ID who will be the author ): Promise { // Create or find collections for categories const collections = new Map(); for (const article of articles) { // Create collection if needed if (!collections.has(article.category)) { const collection = await client.helpCenter.createCollection({ name: article.category, }); collections.set(article.category, collection.id); } // Create article in collection await client.articles.create({ title: article.title, body: article.body, authorId, parentId: collections.get(article.category), state: article.state, }); console.log(`Migrated article: ${article.title}`); await new Promise(r => setTimeout(r, 200)); // Rate limit } } ``` ### Step 5: Migration Orchestrator ```typescript interface MigrationPlan { contacts: SourceContact[]; companies: Array<{ id: string; name: string; plan?: string }>; tags: Array<{ sourceName: string; contactIds: string[] }>; articles: Array<{ title: string; body: string; category: string; state: "published" | "draft" }>; } async function executeMigration(plan: MigrationPlan): Promise { console.log("=== Starting Intercom Migration ==="); const startTime = Date.now(); // Phase 1: Companies (contacts reference these) console.log(`\n[Phase 1] Importing ${plan.companies.length} companies...`); await importCompanies(plan.companies); // Phase 2: Contacts console.log(`\n[Phase 2] Importing ${plan.contacts.length} contacts...`); const contactStats = await importContacts(plan.contacts); console.log(` Created: ${contactStats.created}, Updated: ${contactStats.updated}, Failed: ${contactStats.failed}`); // Phase 3: Tags console.log(`\n[Phase 3] Migrating ${plan.tags.length} tags...`); await migrateTags(plan.tags); // Phase 4: Articles const adminList = await client.admins.list(); const authorId = adminList.admins[0].id; console.log(`\n[Phase 4] Migrating ${plan.articles.length} articles...`); await migrateArticles(plan.articles, authorId); const duration = ((Date.now() - startTime) / 1000 / 60).toFixed(1); console.log(`\n=== Migration complete in ${duration} minutes ===`); if (contactStats.errors.length > 0) { console.log(`\nFailed contacts: ${contactStats.errors.length}`); for (const err of contactStats.errors.slice(0, 10)) { console.log(` ${err.email}: ${err.error}`); } } } ``` ### Step 6: Post-Migration Validation ```typescript async function validateMigration( expectedCounts: { contacts: number; companies: number; tags: number; articles: number } ): Promise<{ passed: boolean; checks: any[] }> { const checks = []; // Check contact count const contacts = await client.contacts.list({ perPage: 1 }); checks.push({ name: "Contact count", expected: expectedCounts.contacts, actual: contacts.totalCount, passed: contacts.totalCount >= expectedCounts.contacts * 0.95, // 95% threshold }); // Check tags exist const tags = await client.tags.list(); checks.push({ name: "Tag count", expected: expectedCounts.tags, actual: tags.data.length, passed: tags.data.length >= expectedCounts.tags, }); // Check articles let articleCount = 0; const articles = await client.articles.list(); for await (const _ of articles) articleCount++; checks.push({ name: "Article count", expected: expectedCounts.articles, actual: articleCount, passed: articleCount >= expectedCounts.articles * 0.95, }); const passed = checks.every(c => c.passed); console.log(`\nValidation: ${passed ? "PASSED" : "FAILED"}`); for (const check of checks) { console.log(` ${check.passed ? "OK" : "FAIL"} ${check.name}: ${check.actual}/${check.expected}`); } return { passed, checks }; } ``` ## Rollback Procedure ```bash # If migration goes wrong: # 1. Stop the migration script # 2. Tag all migrated contacts for identification # 3. Delete migrated contacts if needed: # Search by custom_attributes.migration_date = "today's date" # Delete in batches # Keep source system active during migration # Only decommission after validation + 2 weeks of parallel run ``` ## 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 | ## Resources - [Contacts API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts) - [Companies API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/companies) - [Articles API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/articles) - [Import Contacts Guide](https://developers.intercom.com/docs/guides/tickets/import-contacts) - [Tags API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/tags)