---
title: "HubSpot to Intercom Migration: The CTO's Technical Guide"
slug: hubspot-to-intercom-migration-the-ctos-technical-guide
date: 2026-07-30
author: Nachi
categories: [Migration Guide, HubSpot, Intercom]
excerpt: "A CTO-level technical guide to migrating from HubSpot to Intercom — covering object mapping, API constraints, migration methods, and edge cases."
tldr: "HubSpot-to-Intercom migrations require remapping a relational CRM to a conversation-centric platform. Plan for 1:1 API calls on the Intercom side, data model redesign, and edge case handling."
canonical: https://clonepartner.com/blog/hubspot-to-intercom-migration-the-ctos-technical-guide/
---

# HubSpot to Intercom Migration: The CTO's Technical Guide


# HubSpot to Intercom Migration: The CTO's Technical Guide

A **HubSpot to Intercom migration** moves your customer data from a relational CRM — where Contacts, Companies, Deals, and Tickets live as associated objects with properties and pipelines — to a messenger-first customer platform where everything centers around Contacts, Companies, and Conversations. This is not a 1-to-1 data transfer. HubSpot treats support as an extension of the CRM; Intercom treats the Conversation and the User as the center of gravity, with CRM data acting as context.

This architectural mismatch causes standard export/import processes to fail. If you attempt to map HubSpot objects directly into Intercom without transforming the relational structure, you will lose conversation threading, misalign ticket statuses, and break historical context for your support agents. Intercom needs a **target-state model**, not a field dump. ([intercom.com](https://www.intercom.com/help/en/articles/9396032-historical-data-migration-to-intercom))

This guide covers object mapping, API constraints on both sides, viable migration methods, throughput benchmarks, data residency considerations, and the edge cases that break most DIY attempts.

**Disclosure:** This guide is published by ClonePartner, a migration service provider. The technical content is written to be accurate regardless of which migration approach you choose.

## Why Teams Migrate from HubSpot to Intercom

Teams make this move for platform-level reasons, not data-layer reasons:

- **Messenger-first support.** Intercom's in-product Messenger with persistent conversation history replaces HubSpot's ticket-pipeline model for teams that want real-time, chat-driven customer engagement. HubSpot's live chat exists but is tethered to the CRM object model — conversations live as Engagements on Contact records rather than as first-class threaded objects.
- **Fin AI Agent.** Intercom's LLM-powered AI agent resolves conversations autonomously. As of 2025, Intercom reports Fin resolving up to 50% of support volume for some customers. HubSpot's chatflows are rule-based and tree-structured — they cannot handle open-ended queries or synthesize answers from knowledge base content.
- **Proactive messaging and Product Tours.** In-app lifecycle engagement features — targeted banners, tooltips, multi-step product tours — that HubSpot Service Hub doesn't natively offer.
- **Simpler agent experience.** Teams moving away from HubSpot's CRM-heavy interface where agents navigate multiple object types, property panels, and association tables to resolve a single ticket.
- **Consolidation.** Teams that used HubSpot primarily for support (not sales/marketing) and want a purpose-built support platform rather than paying for a full CRM suite.

For a feature-level comparison, see our [Intercom vs HubSpot Service Hub operations guide](https://clonepartner.com/blog/blog/intercom-vs-hubspot-service-hub-2026-the-operations-leads-guide/). If you are moving Service Hub ticketing specifically, the [HubSpot Service Hub to Intercom migration guide](https://clonepartner.com/blog/blog/hubspot-service-hub-to-intercom-migration-2026-guide/) goes deeper on helpdesk-only scope.

## Core Data Model Differences: HubSpot vs Intercom

The fundamental challenge is a **data model mismatch**. HubSpot is a relational CRM with a deep object graph; Intercom is a communication platform with a flatter schema. ([developers.hubspot.com](https://developers.hubspot.com/docs/api-reference/latest/crm/using-object-apis?utm_source=openai))

| Concept | HubSpot | Intercom |
|---|---|---|
| **People** | Contacts (with Lifecycle Stage: Lead, MQL, Customer, etc.) | Contacts (Users or Leads) |
| **Organizations** | Companies (with properties, associations to Contacts/Deals) | Companies (simpler, associated to Contacts) |
| **Sales Pipeline** | Deals (with Stages, Pipelines, Amount, Close Date) | No native equivalent — use Custom Objects or discard |
| **Support Records** | Tickets (with Pipelines, Stages, SLA properties) | Conversations + Tickets (Ticket Types, Ticket States) |
| **Activities** | Engagements: Notes, Emails, Calls, Tasks, Meetings (each a separate object) | Notes (on Contacts), Conversation Parts |
| **Custom Data** | Custom Objects (full-featured, with associations, pipelines) | Custom Objects (API-populated via Data Connectors, no nested objects) |
| **Relationships** | Associations (two-way, labeled, between any objects) | Contact↔Company, Contact↔Conversation (limited) |
| **Automation** | Workflows (cross-object, trigger on any property change) | Workflows (conversation-centric, Fin-integrated) |
| **Knowledge Base** | Knowledge Base (multi-language, categories, SEO URLs) | Articles (Help Center, collections, multi-language) |

The biggest semantic mismatch: **HubSpot leads and deals are process records; Intercom leads are a contact state.** HubSpot exposes Leads as a dedicated CRM object with stage and score. Intercom defines contacts as Users or Leads and uses Tickets for structured operational work. A HubSpot Lead rarely maps 1:1 to an Intercom Lead, and a HubSpot Deal usually becomes either a Ticket, a set of contact/company attributes, or data that stays in another system of record.

> [!WARNING]
> HubSpot Deals have no native home in Intercom. If your migration scope includes pipeline data, you must either map Deals to Intercom Custom Objects (limited), flatten them into Contact attributes, or accept that Intercom is not your CRM — it's your support layer.

## Data Mapping: HubSpot Objects → Intercom Objects

### Contacts → Contacts

HubSpot Contacts map directly to Intercom Contacts. Intercom distinguishes between **Users** (signed-up, logged-in) and **Leads** (anonymous or pre-signup). You'll need to classify HubSpot contacts based on their Lifecycle Stage.

| HubSpot Property | Intercom Field | Notes |
|---|---|---|
| `email` | `email` | Primary identifier; required for CSV import |
| `firstname` + `lastname` | `name` | Intercom uses a single `name` field — concatenate |
| `phone` | `phone` | Direct map |
| `lifecyclestage` | `role` (user/lead) | Map Customer/Opportunity → User; Subscriber/Lead → Lead |
| `hs_object_id` | `external_id` | Store HubSpot ID for traceability and dedup |
| Custom properties | `custom_attributes.*` | Must pre-create as Data Attributes in Intercom |

### Companies → Companies

HubSpot Companies map to Intercom Companies. Intercom Companies are simpler — fewer native fields, no pipelines, no deal associations.

| HubSpot Property | Intercom Field | Notes |
|---|---|---|
| `name` | `name` | Direct map |
| `domain` | `website` | Direct map |
| `industry` | `industry` | Direct map |
| `numberofemployees` | `size` | Direct map (integer) |
| `hs_object_id` | `company_id` | Use as external identifier for dedup and re-runs |
| `annualrevenue` | `monthly_spend` | Requires transformation (÷ 12) |
| Custom properties | `custom_attributes.*` | Pre-create via Data Attributes API |

> [!NOTE]
> Intercom Companies without at least one associated Contact may not be visible in the UI. Import Companies first, then Contacts, then create the associations.

### Deals → Custom Objects (or Discard)

Intercom has no native Deal/Opportunity object. Your options:

1. **Map to Custom Objects** — Create a Custom Object type (e.g., "Deal") with attributes for stage, amount, close date. Populate via the Custom Object Instances API. Nested objects are not supported; multi-level Deal → Line Item chains must be flattened.
2. **Flatten to Contact attributes** — Store key deal data (last deal amount, stage, close date) as custom attributes on the Contact. Lossy but simple. Works when a Contact typically has one active deal.
3. **Keep in HubSpot or another CRM** — Many teams keep HubSpot CRM for sales and use Intercom only for support/messaging. This is the most common pattern. If you choose this path, set up a sync layer (see Middleware section) to push key deal context to Intercom Contact attributes.

### Tickets → Conversations or Tickets

Intercom has both Conversations and Tickets. HubSpot Tickets can map to either:

- **Intercom Conversations** — Messenger-native, threaded interactions. Best for customer-facing email threads and live chat history.
- **Intercom Tickets** — Structured records with Ticket Types and Ticket States. Best for back-office workflows like bug reports, feature requests, or escalations. Create via `POST /tickets` with a `ticket_type_id`.

You must create Ticket Types in Intercom first, then map HubSpot ticket pipelines/stages to Intercom Ticket States.

| HubSpot Ticket Property | Intercom Equivalent | Notes |
|---|---|---|
| Pipeline | Ticket Type | One Ticket Type per HubSpot pipeline |
| Pipeline Stage | Ticket State | Map each stage to submitted/in_progress/waiting_on_customer/resolved |
| `subject` | Ticket title | Direct map |
| `content` / first engagement | Initial Conversation message | Oldest engagement becomes first message |
| `hs_ticket_priority` | Ticket priority attribute | Map High/Medium/Low |
| `hubspot_owner_id` | `admin_id` (assignee) | Requires an admin ID mapping table |

### Engagements/Activities → Conversation Parts or Notes

HubSpot stores activities as separate Engagement objects (Notes, Emails, Calls, Tasks, Meetings). These must be sequenced chronologically and injected into Intercom:

- **Emails** → Conversation Parts (replies) on the parent Conversation, sorted by timestamp
- **Notes** → Conversation Parts with `type` set to `note` for internal visibility, or standalone Notes on Contact profiles via `POST /contacts/{id}/notes`
- **Calls, Meetings** → Flatten into Notes with metadata (duration, outcome, attendees), or discard
- **Tasks** → No Intercom equivalent; rebuild in your project management tool or discard

The oldest engagement on a ticket becomes the initial Conversation message. Subsequent engagements become Conversation Parts. This threading step is where most DIY migrations encounter the highest error rate — engagement ordering, missing associations, and attachment failures compound here.

### Knowledge Base → Intercom Articles

HubSpot Knowledge Base articles map to Intercom's Help Center Articles. This is a parallel workstream that directly impacts Fin AI's effectiveness:

| HubSpot KB Element | Intercom Equivalent | Notes |
|---|---|---|
| Category | Collection | Create collections first, then assign articles |
| Subcategory | Section (within Collection) | Direct map |
| Article title | Article title | Direct map |
| Article body (HTML) | Article body (HTML) | Intercom supports a subset of HTML; strip unsupported tags |
| Article URL slug | Article URL | Different URL structure — set up 301 redirects |
| Multi-language versions | Translated articles | Intercom supports multi-language Help Centers |

**SEO impact:** HubSpot KB URLs follow the pattern `knowledge.yourdomain.com/article-slug`. Intercom Help Center URLs follow `yourdomain.com/help/en/articles/{id}-slug` or custom domains. You must implement 301 redirects from old URLs to new URLs to preserve search rankings. Export all HubSpot KB URLs before migration and create a redirect map.

**Fin AI dependency:** Fin answers customer questions by referencing your Help Center articles. Migrating KB content accurately — including images, formatting, and internal links — directly determines Fin's resolution quality. Broken articles mean broken AI responses.

### Custom Objects → Flatten or Archive

HubSpot supports Custom Objects with full associations and pipelines. Intercom's Custom Objects are populated via API through Data Connectors and do not support nested objects, pipelines, or multi-level associations. If you have a complex HubSpot architecture (e.g., a custom "Vehicle" object tied to a Contact with sub-objects for "Service History"), you must flatten that data into Contact attributes or a single-level Custom Object in Intercom. Treat this as a redesign exercise, not a port. ([developers.hubspot.com](https://developers.hubspot.com/docs/api-reference/latest/crm/objects/custom-objects/guide?utm_source=openai))

## Migration Approaches

There are five primary ways to move data from HubSpot to Intercom. The right choice depends on your dataset size, engineering bandwidth, and tolerance for data loss.

### 1. Native CSV Export/Import

**How it works:** Export HubSpot records from the UI or Exports API, reshape columns, then import via Intercom's People CSV import. ([knowledge.hubspot.com](https://knowledge.hubspot.com/import-and-export/export-records?hsCtaAttrib=134468176552))

**When to use:** Small, one-time moves where scope is mostly contacts (<10K records) and you don't need history.

**Pros:** No code required. Fast for basic contact data.

**Cons:** Intercom CSV import covers Users and Leads only — not Companies, Conversations, or Tickets. Files max out at 20 MB. Historical activities, relationships, and tickets do not survive. Identifier mistakes create duplicates — Intercom deduplicates by Intercom ID, then `user_id`, then email. ([intercom.com](https://www.intercom.com/help/en/articles/177-import-your-user-data-into-intercom))

**Scalability:** Small only | **Complexity:** Low

### 2. API-Based Migration (In-House)

**How it works:** Extract data via HubSpot's CRM APIs (`/crm/v3/objects/{objectType}`), transform in an ETL layer, then create records via Intercom's REST API (`POST /contacts`, `POST /companies`, `POST /tickets`). ([developers.hubspot.com](https://developers.hubspot.com/docs/api-reference/latest/crm/using-object-apis))

**When to use:** Any migration where you need to preserve relationships, custom fields, conversations, or handle >10K records.

**Pros:** Full control over data transformation. Handles custom objects and complex relationships. Scriptable and repeatable.

**Cons:** Intercom has no bulk import API — every record requires a separate API call. Requires handling rate limits, pagination, and error retries for two different APIs. Edge cases (orphaned engagements, unsupported attachment types, character encoding issues) require constant script adjustments. Expect 2–4 weeks of engineering time for a 100K+ record migration with conversations.

**Scalability:** Enterprise-ready with proper engineering | **Complexity:** High

### 3. Middleware Platforms (Zapier, Make, HubSpot Data Sync)

**How it works:** Use pre-built connectors to sync records between HubSpot and Intercom based on triggers. HubSpot's Data Sync app for Intercom supports real-time one- or two-way sync for users, leads, and companies, but does **not** sync tickets, conversations, or Knowledge Base articles. Intercom's own HubSpot app is one-way from Intercom to HubSpot, not the other direction. ([ecosystem.hubspot.com](https://ecosystem.hubspot.com/marketplace/apps/intercom-data-sync?eco_budget=upper))

**When to use:** Ongoing sync of small volumes post-migration, not one-time historical migration.

**Pros:** No-code/low-code setup. Good for keeping contact and company data in sync after the historical backfill is done.

**Cons:** Not designed for bulk historical migration. iPaaS tools process records one by one — moving 100,000 tickets will trigger massive task overages and likely time out. No association rebuilding. Weak for activities, tickets, conversations, and attachments. Zapier's standard plan allows 750 tasks/month; even the enterprise tier caps at 2M tasks/month, which a large migration will exhaust.

**Scalability:** Small volumes for ongoing sync | **Complexity:** Low–Medium

### 4. Custom ETL Pipeline

**How it works:** Build an extractor, transformer, loader with a persistent ID map, retry queue, and validation layer. The safest pattern is HubSpot → staging database/warehouse → Intercom, with resumable jobs and an exceptions table.

**When to use:** Enterprise governance, coexistence scenarios, or repeated M&A-style migrations. Also appropriate if you already have a mature modern data stack (dbt, Airflow, Fivetran) and treat your warehouse as the source of truth.

**Pros:** Best observability and resumability. Highly auditable. Data transformations are version-controlled. Supports incremental delta syncs natively.

**Cons:** Expensive to build and maintain — expect 4–8 weeks of engineering effort for a full-scope pipeline. Overkill for a one-off migration. Reverse ETL tools (Census, Hightouch) often struggle with the nuances of Intercom's Conversation Parts threading.

**Scalability:** Enterprise | **Complexity:** High

### 5. Third-Party Migration Tools or Managed Services

**How it works:** A vendor or SaaS tool extracts HubSpot data through APIs, maps it to Intercom, validates, and runs the migration. Intercom's own migration guidance explicitly treats third-party providers as a valid route. ([developers.intercom.com](https://developers.intercom.com/docs/guides/tickets/import-tickets))

**When to use:** Fixed deadline, large history, low internal engineering bandwidth, or high cost of mistakes.

**Pros:** Fastest turnaround (typically 1–2 weeks vs. 4–8 weeks in-house). Better rollback/runbook discipline. No internal engineering bandwidth consumed. Validation included.

**Cons:** Tool-only providers often handle standard objects better than custom ones. Rigid schema — if your data doesn't fit their predefined mapping, it may be skipped silently. You must verify exactly what will be recreated versus archived. Costs vary significantly: expect $2,000–$15,000+ depending on record volume, object complexity, and whether conversations with threading are in scope.

**Scalability:** Small to Enterprise (provider-dependent) | **Complexity:** Low (for you)

### Comparison of Migration Methods

| Method | Best For | History & Relationships | Engineering Effort | Scale | Typical Timeline |
|---|---|---|---|---|---|
| CSV Export/Import | Contacts-only move | No | Zero | Small (<10K) | 1–2 days |
| API (In-House) | Complex, custom setups | Yes | High (2–4 weeks) | Any | 2–6 weeks |
| Middleware / Data Sync | Ongoing sync only | Limited | Low | Small–Mid | Ongoing |
| Custom ETL Pipeline | Governed enterprise | Yes | High (4–8 weeks) | Enterprise | 4–10 weeks |
| Managed Service | Deadline-driven, complex | Yes (provider-dependent) | Low | Any | 1–3 weeks |

**Recommended pattern by scenario:**

- **Small business, contacts only:** CSV is acceptable if you don't need companies, history, or tickets.
- **Enterprise or multi-object move:** API pipeline or managed service.
- **Migration plus ongoing coexistence:** Managed migration for the historical backfill, then Data Sync or middleware for deltas.
- **Low engineering bandwidth:** Do not build a custom ETL unless migration failure is cheaper than lost product velocity.

## API Constraints You Must Plan For

### HubSpot (Source)

- **Private apps:** 100–190 requests per 10 seconds depending on subscription tier (Starter: 100/10s, Professional: 150/10s, Enterprise: 200/10s), with daily limits of 250K–1M requests. ([developers.hubspot.com](https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines))
- **CRM Search API:** Has its own lower rate cap (4 requests/second for Private apps) and is limited to **10,000 results per query**. For datasets larger than 10K, chunk by `lastmodifieddate` or created date ranges. ([developers.hubspot.com](https://developers.hubspot.com/docs/api-reference/latest/crm/search-the-crm))
- **Pagination:** All list endpoints use cursor-based pagination (`after` parameter), returning up to 100 records per page.
- **Activities:** Engagements cannot be exported via the standard CRM UI export — they require separate API calls via the Associations or Engagements API. For a ticket with 50 email engagements, that's 50+ additional API calls per ticket.
- **Associations API (v4):** Required to traverse object relationships. Returns up to 500 associations per request. Multi-hop queries (Ticket → Contact → Company) require chained calls.

### Intercom (Target)

- **Rate limits:** 1,000 API calls per minute for most endpoints (varies by plan — Starter plans may have lower limits). Workspaces can request elevated limits from Intercom support for migrations.
- **No bulk create endpoint** for Contacts, Companies, or Tickets — every record is a separate API call.
- **409 Conflict** returned if you attempt to create a Contact that already exists (match on email or external_id). Build search-then-update logic.
- **Custom attributes** must be pre-created as Data Attributes before you can write values to them. The API silently drops values for attributes that don't exist — no error is returned.
- **Ticket Parts** limited to 500 per ticket.
- **Conversation threading:** You must create the initial message, wait for the response to get the `conversation_id`, then iterate through subsequent messages to append them as replies. You cannot POST an entire threaded conversation in a single API call. Each reply is a separate API call.
- **CSV import** requires email as a mandatory field — contacts without email cannot be imported via CSV.
- **Scroll API** for listing contacts is deprecated in recent API versions; use search endpoints instead.

### Throughput Benchmarks

At Intercom's standard rate limits, realistic migration throughput:

| Record Type | API Calls per Record | Throughput at 1K calls/min | Time for 100K Records |
|---|---|---|---|
| Contacts (create) | 1 | ~1,000/min | ~100 minutes |
| Companies (create) | 1 | ~1,000/min | ~100 minutes |
| Contact↔Company associations | 1 | ~1,000/min | ~100 minutes |
| Conversations (5 parts avg) | 6 (1 create + 5 replies) | ~166 conversations/min | ~10 hours |
| Conversations (20 parts avg) | 21 | ~47 conversations/min | ~35 hours |

**Key implication:** Conversation threading is the bottleneck. A migration of 50,000 tickets averaging 10 conversation parts each requires ~500,000 API calls to Intercom — approximately 8+ hours at standard rate limits, assuming zero errors or retries. Request elevated rate limits from Intercom support before starting any migration exceeding 100K total API calls.

> [!TIP]
> For large migrations, request elevated rate limits from Intercom support before starting. Provide your estimated total API call volume and desired migration window. Intercom typically responds within 2–3 business days.

## Migration Architecture

The architecture follows a strict **Extract → Transform → Load → Validate** pattern, buffered by a staging database for rate limit handling and state management.

1. **Extract:** Use HubSpot's CRM APIs for repeatable extraction. For Engagements, use the Associations API. Store raw JSON payloads in a staging database (PostgreSQL is preferred for its JSON querying capabilities). For quick reconciliation, also export CSV snapshots of Contacts/Companies views. ([knowledge.hubspot.com](https://knowledge.hubspot.com/import-and-export/export-records?hsCtaAttrib=134468176552))
2. **Transform:** Map the JSON schema. Concatenate name fields. Convert HubSpot timestamps (milliseconds since epoch) to Unix timestamps (seconds since epoch) — dividing by 1000. Convert HubSpot's rich text (often HTML with inline styles) into Intercom's supported HTML subset. Download attachment files and prepare them for Intercom's upload endpoint. Validate email uniqueness. Strip or transform unsupported characters.
3. **Load:** Push to Intercom in dependency order: **Data Attributes and Ticket Types first, then Contacts, then Companies, then Contact↔Company associations, then Tickets/Conversations with threaded Parts, then assignments and final state transitions.** Store the resulting Intercom IDs in your staging database, mapped to original HubSpot IDs. ([intercom.com](https://www.intercom.com/help/en/articles/9396032-historical-data-migration-to-intercom))
4. **Validate:** Compare record counts, spot-check field values, verify associations, then run a final delta batch for data changed after the initial load.

> [!WARNING]
> Intercom's migration guidance warns that message-by-message history replay can multiply API volume by 10–100x versus creating a single conversation per ticket. For teams with >50K tickets, the pragmatic compromise is one source ticket = one Intercom Conversation with a consolidated transcript body, rather than every message as a distinct Conversation Part. This trades per-message fidelity for completion speed and API budget. ([intercom.com](https://www.intercom.com/help/en/articles/9396032-historical-data-migration-to-intercom))

## Step-by-Step Migration Process

### Step 1: Audit and Scope

Query HubSpot to determine exact record counts for Contacts, Companies, Tickets, Engagements, KB Articles, and Custom Objects. Identify:

- Active vs. archived records (HubSpot keeps archived records accessible via API with `archived=true`)
- Custom objects and their associations (query `GET /crm/v3/schemas` for the full object graph)
- Custom properties with non-standard data types (calculated fields, rollup properties won't have equivalents)
- Activities you actually need — most teams skip Tasks and Meetings, which reduces API volume by 20–40%
- Duplicate records (use HubSpot's built-in dedup tool before exporting)
- Average engagements per ticket — this determines your API call budget for the conversation threading phase

Define what moves, what stays, and what gets archived. Every field you keep becomes mapping, testing, and support cost later. Prune unnecessary historical data — closed tickets older than 2–3 years rarely provide agent value but can double migration time.

### Step 2: Create the Target Schema in Intercom

Before loading any data:

1. Create custom Data Attributes for Contacts and Companies via `POST /data_attributes`. Specify `data_type` (string, integer, boolean, date) — mismatches cause silent value drops.
2. Create Ticket Types that map to your HubSpot ticket pipelines via `POST /ticket_types`
3. Create Custom Object types if migrating Deals or other HubSpot custom objects
4. Create Tags that map to HubSpot list memberships or lifecycle stages
5. Create Help Center collections that map to HubSpot KB categories
6. Map HubSpot owners to Intercom admin IDs — build a lookup table of `hubspot_owner_id` → `intercom_admin_id`

### Step 3: Extract from HubSpot

```python
import requests

HUBSPOT_TOKEN = "your-token"
BASE_URL = "https://api.hubapi.com/crm/v3/objects"

def extract_hubspot_objects(object_type, properties):
    results = []
    after = None
    while True:
        params = {"limit": 100, "properties": properties}
        if after:
            params["after"] = after
        resp = requests.get(
            f"{BASE_URL}/{object_type}",
            headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
            params=params
        )
        resp.raise_for_status()
        data = resp.json()
        results.extend(data.get("results", []))
        paging = data.get("paging", {}).get("next")
        if not paging:
            break
        after = paging["after"]
    return results

contacts = extract_hubspot_objects(
    "contacts",
    ["email", "firstname", "lastname", "phone", "lifecyclestage"]
)
```

For each ticket, query the Associations API to fetch related Engagements:

```python
def get_ticket_engagements(ticket_id):
    url = f"https://api.hubapi.com/crm/v4/objects/tickets/{ticket_id}/associations/emails"
    response = requests.get(
        url, headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"}
    )
    if response.status_code == 200:
        return response.json().get("results", [])
    return []
```

For Knowledge Base articles:

```python
def extract_kb_articles():
    url = "https://api.hubapi.com/cms/v3/blogs/posts"
    # Note: HubSpot KB uses the CMS API, not the CRM API
    results = []
    after = None
    while True:
        params = {"limit": 100}
        if after:
            params["after"] = after
        resp = requests.get(
            url, headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
            params=params
        )
        data = resp.json()
        results.extend(data.get("results", []))
        paging = data.get("paging", {}).get("next")
        if not paging:
            break
        after = paging["after"]
    return results
```

### Step 4: Transform

- Concatenate `firstname` + `lastname` → `name` (handle null values — `None Smith` is a common bug)
- Map `lifecyclestage` → Intercom `role` (user/lead)
- Convert HubSpot timestamps (milliseconds) to Unix timestamps (seconds): `intercom_ts = hubspot_ts // 1000`
- Validate email uniqueness (Intercom rejects duplicates with 409)
- Map picklist values to Intercom custom attribute options
- Sort Engagements chronologically — the oldest becomes the initial Conversation message, the rest become Conversation Parts
- Strip unsupported HTML tags from KB articles (Intercom Articles support a limited HTML subset)
- Download and re-host inline images from HubSpot's CDN (see Edge Cases)
- Build the owner mapping: `hubspot_owner_id` → `intercom_admin_id`

### Step 5: Load into Intercom

Create Contacts first, handling duplicates and rate limits:

```python
import time

INTERCOM_TOKEN = "your-token"
INTERCOM_HEADERS = {
    "Authorization": f"Bearer {INTERCOM_TOKEN}",
    "Content-Type": "application/json",
    "Intercom-Version": "2.11"
}

def create_intercom_contact(contact_data, retry_count=0):
    resp = requests.post(
        "https://api.intercom.io/contacts",
        headers=INTERCOM_HEADERS,
        json=contact_data
    )
    if resp.status_code == 409:
        return search_and_update(contact_data)
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 10))
        time.sleep(retry_after)
        if retry_count < 5:
            return create_intercom_contact(contact_data, retry_count + 1)
        raise Exception(f"Rate limit exceeded after {retry_count} retries")
    resp.raise_for_status()
    return resp.json()

def search_and_update(contact_data):
    """Search for existing contact and update instead of create."""
    search_resp = requests.post(
        "https://api.intercom.io/contacts/search",
        headers=INTERCOM_HEADERS,
        json={
            "query": {
                "field": "email",
                "operator": "=",
                "value": contact_data["email"]
            }
        }
    )
    results = search_resp.json().get("data", [])
    if results:
        contact_id = results[0]["id"]
        update_resp = requests.put(
            f"https://api.intercom.io/contacts/{contact_id}",
            headers=INTERCOM_HEADERS,
            json=contact_data
        )
        return update_resp.json()
    return None
```

Use structured logs that capture `source_id`, `target_id`, `attempt`, `status_code`, and `payload_hash`. Store these in your staging database — this is what makes partial reruns safe and rollbacks possible.

### Step 6: Rebuild Relationships

After loading Contacts and Companies separately, attach them:

```python
def attach_contact_to_company(contact_id, company_id):
    resp = requests.post(
        f"https://api.intercom.io/contacts/{contact_id}/companies",
        headers=INTERCOM_HEADERS,
        json={"id": company_id}
    )
    if resp.status_code == 404:
        log_error(f"Contact {contact_id} or Company {company_id} not found")
    return resp
```

Then inject Conversations:

1. POST the initial message to create a Conversation and capture the returned `conversation_id`
2. Loop through remaining Conversation Parts chronologically and POST them to `/conversations/{id}/reply`
3. For each Part, set `admin_id` to the correct mapped agent (not the API key owner)
4. For attachments, download from HubSpot, upload to Intercom's file upload endpoint, and embed the resulting URL in the conversation part
5. After all Parts are loaded, set the final conversation state (open/closed/snoozed) and assignment

### Step 7: Validate

- Compare record counts: HubSpot source vs. Intercom target
- Spot-check 5–10% of records for field-level accuracy
- Verify Company↔Contact associations
- Confirm custom attribute values populated correctly (especially date and number types)
- Test Ticket Types and states match expected pipeline mapping
- Verify agent attribution on historical conversations
- Check inline images render correctly in migrated conversations and articles
- Run UAT: have support agents locate specific historical tickets and verify thread readability

### Step 8: Delta Sync and Cutover

If your migration takes 48+ hours, new records will be created in HubSpot during that window. Build a delta script that queries HubSpot for `lastmodifieddate > start_of_migration` to catch stragglers before the final cutover.

**Cutover strategies:**

- **Big Bang (recommended for most teams):** Schedule over a weekend. Freeze HubSpot writes at COB Friday, run delta sync Saturday, validate Sunday, go live Monday. Downside: compressed timeline for troubleshooting.
- **Phased (by team/department):** Move one support team at a time. Requires complex routing rules to direct incoming conversations to the correct platform during the transition period. Only viable if teams have cleanly separated ticket queues.
- **Parallel run (2–4 weeks):** Run both platforms simultaneously with manual reconciliation. Expensive in agent time but lowest risk. Best for enterprises with >500K records or regulated industries.

## GDPR and Data Residency Considerations

For organizations handling EU customer data, the migration introduces data processing obligations:

- **Data Processing Agreements (DPAs):** Verify that both HubSpot and Intercom DPAs cover data transfer between processors. If using a third-party migration tool, they also need a DPA as a sub-processor.
- **Data residency:** HubSpot offers EU data hosting (Frankfurt). Intercom hosts data in the US (AWS us-east-1) by default, with EU hosting available for eligible plans. Confirm that your Intercom workspace is provisioned in the correct region before migration.
- **Right to erasure:** If a GDPR deletion request arrives mid-migration, you must be able to identify and purge that record from your staging database, HubSpot, and Intercom. Your ID mapping table enables this.
- **Lawful basis for transfer:** The migration itself is a processing activity. Document it in your Record of Processing Activities (ROPA). If your lawful basis is consent, verify that consent covers the new processor (Intercom).
- **Data minimization:** Migration is an opportunity to purge data you no longer need. Don't migrate inactive contacts from 2017 just because they exist in HubSpot.
- **Transit encryption:** Both HubSpot and Intercom APIs use TLS 1.2+. If your staging database is in the cloud, ensure it's encrypted at rest and in transit.

## Multi-Workspace and Multi-Portal Scenarios

Enterprise organizations often have:

- **Multiple HubSpot portals** (e.g., one per business unit or acquired company)
- **Multiple Intercom workspaces** (e.g., separate workspaces for different products)

In multi-portal scenarios, deduplicate contacts across portals before migration — the same customer may exist in multiple HubSpot instances with different `hs_object_id` values. Use email as the dedup key, and designate one portal as the source of truth for each contact's properties.

For multi-workspace Intercom targets, each workspace has independent rate limits and API keys. You can parallelize migrations across workspaces, but cross-workspace data sharing is limited in Intercom.

## Edge Cases That Break Migrations

- **The "Author" Problem.** When migrating historical conversations, Intercom defaults to showing the API key owner as the author of all messages. You must explicitly set the `admin_id` in the API payload to ensure historical replies are attributed to the correct agents. This requires building a mapping table of HubSpot owner IDs to Intercom admin IDs before starting the load phase.

- **Inline Images.** HubSpot often hosts inline images behind authenticated URLs or on its CDN (`f.hubspotusercontent.com`). If you migrate the HTML `<img>` tag as-is, the image will break in Intercom because the agent doesn't have the HubSpot session token, or because HubSpot may expire the URL. You must download and re-host every inline image — either to Intercom's upload endpoint or to your own CDN.

- **Orphaned Engagements.** HubSpot allows engagements (notes, tasks) to exist without a parent ticket. Intercom requires all communication to belong to a Conversation or be a standalone Note on a User profile. Build routing logic: if an engagement has a ticket association, attach to the corresponding Conversation; if it only has a contact association, create as a Note on the Contact; if it has no associations, log to an exceptions table for manual review.

- **Contacts without email.** Intercom CSV import requires email. API import can use `external_id` instead, but the contact won't be searchable by email in the UI and won't match on inbound emails. Decide whether these contacts are worth migrating.

- **Duplicate contacts.** HubSpot allows multiple contacts with the same email in some edge cases (particularly after imports or integrations). Intercom enforces strict email uniqueness — you'll get 409s. Deduplicate in HubSpot or merge in your transform step before loading. ([intercom.com](https://www.intercom.com/help/en/articles/177-import-your-user-data-into-intercom))

- **Multi-level associations.** HubSpot's Company → Contact → Deal → Line Item chains don't translate. Intercom has a flatter model — you'll need to flatten or discard layers. A common approach: store the Company and Contact relationship, flatten Deal data onto the Contact, discard Line Items.

- **Property history loss.** HubSpot tracks property change history on records (e.g., every lifecycle stage transition with timestamp). Intercom does not store property history. You migrate current state only. If you need historical property data for analytics, export it to your data warehouse before migration.

- **Historical timestamps.** Intercom's API accepts `created_at` when creating conversations, allowing you to preserve original creation dates. However, subsequent Conversation Parts (replies) may default to the API call timestamp depending on the endpoint and API version. Test with a small batch and verify timestamps before running at scale.

- **Attachment re-hosting.** HubSpot engagement attachments must be downloaded and re-uploaded. Intercom conversation replies support attachment URLs, but files hosted on HubSpot's servers may become inaccessible after you decommission HubSpot. Download all attachments during the extract phase and re-host permanently. ([developers.intercom.com](https://developers.intercom.com/docs/references/2.0/rest-api/conversations/reply-to-a-conversation))

- **Rich text and HTML formatting.** HubSpot emails often contain complex HTML with inline CSS, tables, and custom fonts. Intercom's Conversation Parts support a limited HTML subset. Unsupported tags are stripped silently, which can break formatting. Run a transform step that converts complex HTML to Intercom-compatible markup.

- **Workflow triggers on migrated data.** If Intercom workflows are active during migration, creating conversations via API can trigger auto-replies, assignments, SLA timers, and customer notifications. Disable or add API-source exceptions to all workflows before loading historical data.

## Validation and Testing Checklist

Do not trust a 200 OK response blindly. Implement rigorous validation:

| Check | Method | Pass Criteria |
|---|---|---|
| Contact count | API count comparison | ±0 records (after intentional exclusions) |
| Company count | API count comparison | ±0 records |
| Contact↔Company links | Sample 50+ records | 100% match |
| Custom attribute accuracy | Sample 5–10% of records | All values match source |
| Ticket/Conversation count | Count comparison | ±0 records |
| Conversation Parts per ticket | Sample 20 tickets | Part count matches engagement count |
| Agent attribution | Sample 20 conversations | Correct agent shown, not API key owner |
| Note content | Spot-check 20 records | Body text intact, no encoding issues |
| Tags applied correctly | Filter by tag in Intercom | Expected segment sizes match |
| KB article count | Count comparison | ±0 articles |
| KB inline images | Spot-check 10 articles | All images render correctly |
| Thread readability | UAT with support agents | Agents can find and read historical tickets |
| Timestamps | Sample 10 records | `created_at` matches source within acceptable tolerance |

**Rollback plan:** Maintain the ID mapping table in your staging database (HubSpot ID → Intercom ID). If validation fails, use this table to execute targeted deletes:

```python
def rollback_contacts(mapping_table):
    """Delete all migrated contacts using the mapping table."""
    for row in mapping_table:
        resp = requests.delete(
            f"https://api.intercom.io/contacts/{row['intercom_id']}",
            headers=INTERCOM_HEADERS
        )
        if resp.status_code == 200:
            log_success(f"Deleted {row['intercom_id']}")
        else:
            log_error(f"Failed to delete {row['intercom_id']}: {resp.status_code}")
```

Never run a migration without a tested rollback mechanism.

## Post-Migration Tasks

Data is only half the migration. The operational layer must be rebuilt.

1. **Rebuild automations.** HubSpot Workflows do not migrate. Manually rebuild routing rules, SLAs, and auto-responders in Intercom's Workflow builder. Document each HubSpot workflow, identify its Intercom equivalent, and test before enabling. Confirm that migration-created items do not trigger customer-visible notifications.

2. **Configure Fin AI.** Point Intercom's Fin AI at your migrated Help Center articles for automated resolutions. Verify that article formatting survived migration — Fin's answer quality depends on clean, well-structured content. Test Fin with 20–30 representative customer questions and compare answers against expected responses.

3. **Set up 301 redirects for KB URLs.** If your HubSpot KB was publicly indexed, implement redirects from old URLs to new Intercom Help Center URLs. This preserves SEO value and prevents broken links in customer bookmarks, emails, and external documentation.

4. **Set up integrations.** Reconnect third-party tools (Slack, Salesforce, Stripe, Jira) to Intercom. If keeping HubSpot CRM for sales, set up a sync layer using Data Sync or middleware to push deal context to Intercom Contact attributes.

5. **Train your team.** Intercom's inbox is fundamentally different from HubSpot's ticket view. Key differences agents need to learn:
   - Keyboard shortcuts for reply, note, assign, and close
   - Macros (Intercom calls them "Saved Replies") vs. HubSpot snippets
   - The distinction between internal notes and public replies (different from HubSpot's model)
   - How to search for and reference historical migrated conversations
   - Allocate 3–5 days of structured onboarding before cutover

6. **Monitor for 30 days.** Watch for missing records, broken associations, and automation gaps. Set up alerts for:
   - Conversations with no assigned agent
   - Contacts missing company associations
   - Custom attribute values that are null when they shouldn't be
   - Fin AI resolution rate (baseline against pre-migration support metrics)

For a detailed post-cutover testing workflow, see our [Post-Migration QA checklist](https://clonepartner.com/blog/blog/help-desk-data-migration-qa-checklist/).

## Best Practices

- **Run a sandbox migration first.** Migrate a 5% subset to an Intercom test workspace. This exposes mapping errors, timestamp issues, and formatting problems before they pollute production. Most teams discover 3–5 critical mapping errors during sandbox testing.
- **Back up everything.** Export all HubSpot data to CSV as a baseline snapshot before you start transforming. Store in a durable location (S3, GCS) with a 90-day retention policy.
- **Pre-create all custom attributes** in Intercom before loading data. The API silently drops values for attributes that don't exist — you will not get an error, and you will not know data was lost until validation.
- **Use `external_id` on every record.** Store HubSpot's `hs_object_id` as the `external_id` in Intercom for traceability, dedup, and rollback.
- **Freeze HubSpot configurations.** Implement a change freeze two weeks before migration. Don't allow admins to add custom properties, change pipeline stages, or modify list criteria.
- **Handle 409s gracefully.** Build search-then-update logic for duplicate contacts instead of failing the entire batch. Log every 409 for post-migration review.
- **Monitor rate limits via response headers.** Both HubSpot (`X-HubSpot-RateLimit-Remaining`) and Intercom (`X-RateLimit-Remaining`) return remaining quota in response headers — use them to implement adaptive backoff rather than fixed sleep intervals.
- **Test with ugly records.** Don't validate only with clean data. Use records with null owners, stale stages, malformed phone numbers, deleted associations, emoji in names, and HTML in plain text fields.
- **Suppress workflows during historical loads.** Ensure Intercom workflows have exceptions for items created via API so migrated records don't trigger customer notifications. Add a `migrated: true` tag or custom attribute to migrated records and exclude them from workflow triggers. ([intercom.com](https://www.intercom.com/help/en/articles/9396032-historical-data-migration-to-intercom))
- **Plan for the 80/20 rule of migration effort.** Contacts and companies typically represent 20% of the work. Conversation threading, attachment re-hosting, and agent attribution consume 80% of the engineering effort. Scope accordingly.

## When to Use a Managed Migration Service

**Build in-house if:**
- Your dataset is <10K contacts with minimal custom objects
- You have a dedicated engineer with 2–4 weeks of availability
- Your data model is flat (no multi-level associations, no conversation history to preserve)
- You can tolerate a second attempt if the first run fails

**Use a managed service if:**
- You have >50K records across multiple objects
- Custom objects, multi-level associations, or complex field transformations are involved
- You need the migration completed in days, not weeks
- Engineering bandwidth is already committed to product work
- You need conversation threading with correct agent attribution, attachment re-hosting, or KB migration with 301 redirects
- Regulatory requirements (GDPR, SOC 2) demand an auditable migration process with documented validation

The hidden cost of DIY migrations isn't the API code — it's the edge case handling, validation, and re-runs when something breaks at record 47,000 out of 200,000.

> Planning a HubSpot to Intercom migration with conversations, custom objects, or Knowledge Base articles in scope? Book a 30-minute technical scoping call to discuss your data model and migration timeline.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I migrate HubSpot tickets to Intercom using a CSV?

No. Intercom's CSV import covers Users and Leads only — not Companies, Conversations, or Tickets. You must use the Intercom REST API to migrate historical support data, creating each record individually since there is no bulk import endpoint.

### Can I migrate HubSpot Deals to Intercom?

Not 1:1. Intercom has no native Deal/Opportunity object. You can map operationally useful deal data to Intercom Custom Objects or Ticket attributes, flatten key data into Contact custom attributes, or keep Deals in HubSpot CRM and use Intercom only for support.

### Does Intercom have a bulk import API?

No. Intercom does not offer a bulk create or update endpoint for Contacts, Companies, or Tickets. Every record requires a separate API call. For large migrations, request elevated rate limits from Intercom support before starting.

### How long does a HubSpot to Intercom migration take?

A small migration (<10K contacts, no custom objects) can be completed in 1–3 days. Enterprise migrations with 100K+ records, custom objects, and relationship rebuilding typically take 2–4 weeks including validation and testing.

### Will I lose HubSpot property change history when migrating to Intercom?

Yes. HubSpot tracks property change history on records. Intercom does not have an equivalent feature. You migrate the current state of each record only. If you need historical data, export and archive it separately before migration.
