Skip to content

HubSpot to Intercom Migration: The CTO's Technical Guide

A CTO-level technical guide to migrating from HubSpot to Intercom — covering object mapping, API constraints, migration methods, and edge cases.

Nachi Nachi · · 26 min read
HubSpot to Intercom Migration: The CTO's Technical Guide
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

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)

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. If you are moving Service Hub ticketing specifically, the HubSpot Service Hub to Intercom migration 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)

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
Info

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)

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)

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)

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)

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)

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)

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)
  • 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)
  • 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)
  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)
  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)

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_idintercom_admin_id

Step 3: Extract from HubSpot

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:

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:

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 + lastnamename (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_idintercom_admin_id

Step 5: Load into Intercom

Create Contacts first, handling duplicates and rate limits:

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:

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)

  • 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)

  • 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:

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.

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)
  • 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.

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.

More from our Blog