---
title: Salesforce Service Cloud to HubSpot Service Hub Migration Guide
slug: salesforce-service-cloud-to-hubspot-service-hub-migration-guide
date: 2026-07-22
author: Raaj
categories: [Migration Guide, Salesforce Service Cloud, Hubspot Service Hub]
excerpt: "Technical guide to migrating Salesforce Service Cloud to HubSpot Service Hub: object mapping, API constraints, migration methods, edge cases, and validation."
tldr: "Salesforce to HubSpot Service Hub migration requires API-based ETL for anything beyond flat records. Custom objects need Enterprise, and relationships must load in dependency order."
canonical: https://clonepartner.com/blog/salesforce-service-cloud-to-hubspot-service-hub-migration-guide/
---

# Salesforce Service Cloud to HubSpot Service Hub Migration Guide


> [!NOTE]
> **TL;DR** — Migrating from Salesforce Service Cloud to HubSpot Service Hub is a data-model translation project, not a drag-and-drop export. Salesforce Cases, Case Comments, Email Messages, and custom objects do not map 1:1 to HubSpot Tickets and Engagements. CSV imports break multi-object relationships. For anything beyond flat records, you need API-based ETL — extracting via Salesforce Bulk API 2.0, transforming the relational structure, and loading into HubSpot via batch endpoints. Custom objects require HubSpot Enterprise, and some Salesforce objects (Entitlements, Milestones) have no HubSpot equivalent at all.

Salesforce Service Cloud organizes customer support around the **Case** object — with lookup relationships to Accounts, Contacts, Assets, Entitlements, Milestones, EmailMessages, and CaseComments. HubSpot Service Hub centers on the **Ticket** object associated with Contacts, Companies, and Engagements. The two schemas don't map 1:1, and the migration's difficulty scales with your org's custom objects, automation rules, and SLA complexity.

This guide covers every viable migration method, the object mapping that matters, the API constraints you'll hit, throughput benchmarks, error handling specifics, and the edge cases that break migrations in production.

## Why Teams Migrate from Salesforce Service Cloud to HubSpot Service Hub

Three drivers show up repeatedly in migration projects:

**Cost consolidation.** Service Cloud Enterprise lists at $165 per user/month (2026, billed annually), following Salesforce's ~9% price increase in late 2025. Service Cloud also carries the heaviest add-on attach in the Salesforce portfolio: Digital Engagement ($75/user/month), Service Cloud Voice ($50/user/month), Field Service ($150/user/month), and Einstein for Service ($50/user/month) each layer on at list price. HubSpot Service Hub Professional starts at $100/month for 5 users ($20/user/month equivalent); Enterprise starts at $150/month for 10 users ($15/user/month equivalent). For teams also running Sales Cloud, Pardot, and a sales engagement tool, a Salesforce line item plus marketing automation plus sales engagement plus admin salary produces a very different TCO number than the license alone. Consolidating several tools into one platform is where most of the cost argument lives.

**Simpler administration.** HubSpot's UI requires significantly less training than Salesforce. Reps typically reach functional proficiency in 1–2 days versus 1–2 weeks for Salesforce. Higher adoption rates follow — organizations report 15–30% higher daily active usage after switching from Salesforce to HubSpot, particularly among non-technical support agents. For companies struggling with CRM adoption, this alone can justify migration.

**Platform unification.** HubSpot offers CRM, marketing automation, sales engagement, service/support, and CMS in one platform with native integration. Salesforce requires stitching together multiple tools (Sales Cloud + Pardot/Marketing Cloud + Outreach + Service Cloud) with API integrations that require ongoing maintenance. Consolidation benefit: one login, one dataset, one support vendor, one contract, one user permission model.

### When HubSpot Service Hub Is Not the Right Target

Be honest about what you're giving up:

- **SLA complexity.** HubSpot offers SLA management for Service Hub Professional and above, but limits you to three SLA policies based on priority levels. Salesforce supports unlimited Entitlement Processes with multi-tier Milestones, business-hours-aware calculations, and escalation actions at each stage. If you run contractual SLAs with different response/resolution targets per customer tier, product line, and support channel, HubSpot cannot replicate this.
- **Routing and assignment.** HubSpot's support routing uses round-robin and skill-based assignment at the Professional tier. Salesforce's Omni-Channel routing supports queue-based, skills-based, and capacity-based routing with real-time presence tracking and configurable routing priority. If you need routing decisions based on agent capacity, skill weight, and queue backlog simultaneously, HubSpot's routing is insufficient.
- **Field Service Management.** Salesforce Field Service (dispatching, work orders, mobile workforce) has no HubSpot equivalent.
- **Entitlements and Milestones.** Salesforce's Entitlement and Milestone objects — contractual support commitments with automated tracking — have no HubSpot counterpart. No amount of custom object workaround fully closes this gap.
- **SLA visibility.** HubSpot's SLA statuses are only visible within HubSpot's Conversations function, so they do not apply to tickets unless each ticket is connected to a conversation within your inbox. Tickets created via API, form submission, or manual creation outside of Conversations do not trigger SLA tracking.

## Core Data Model Differences: Salesforce Service Cloud vs HubSpot Service Hub

Understanding these differences before you write a single script prevents 80% of migration failures.

| Salesforce Service Cloud | HubSpot Service Hub | Notes |
|---|---|---|
| Account | Company | 1:1 mapping. HubSpot uses domain-based deduplication. |
| Contact | Contact | Direct map. Salesforce allows one Account per Contact; HubSpot allows multiple Company associations. |
| Lead | Contact (with Lifecycle Stage) | HubSpot has no separate Lead object. Map Leads to Contacts and set `lifecyclestage = lead`. |
| Case | Ticket | Primary service object. Map Case Status → Ticket Pipeline Stage. Map RecordTypeId → Pipeline. |
| Opportunity | Deal | Map Stage → Deal Pipeline Stage. |
| EmailMessage | Email Engagement | Case emails become email engagements associated to the ticket. Threading via `ThreadIdentifier` must be handled explicitly (see Edge Cases). |
| CaseComment | Note Engagement | CaseComments become notes on the ticket timeline. Preserve `CreatedById` and `CreatedDate` in the note body. |
| Task / Event | Task / Meeting | Direct map to HubSpot activity types. |
| Knowledge Article (KnowledgeArticleVersion) | Knowledge Base Article | Requires re-creation; no bulk import API for KB articles. |
| Entitlement / Milestone | No equivalent | Must be rebuilt as custom properties, stored in an external system, or dropped. |
| Asset | Custom Object (Enterprise only) | Requires HubSpot Enterprise. |
| Service Contract | No equivalent | Flatten into custom properties on Company or create a custom object. |
| Custom Objects | Custom Objects (Enterprise only) | HubSpot custom objects are available on Enterprise plans only. An Enterprise subscription allows up to 10 custom object definitions with up to 500,000 records each without additional charges. |

> [!WARNING]
> **Custom objects require HubSpot Enterprise.** As of mid-2025, Custom Objects remain Enterprise-only across every Hub. If your Salesforce org relies heavily on custom objects and you're targeting HubSpot Professional, those objects have no destination. Plan for this before you scope the project.

## Migration Approaches: Methods, Trade-offs, and When to Use Each

### 1. Native CSV Export/Import

**How it works:** Use Salesforce Data Export Service, Data Loader, or Reports to export objects as CSV files. Clean and transform in a spreadsheet or script. Import into HubSpot via the native import wizard.

**When to use it:** Small datasets under 50,000 records with simple relationships. Teams with no developer bandwidth who need a quick migration of Contacts, Companies, and basic Tickets.

**Pros:**
- No code required
- HubSpot's import wizard handles basic deduplication
- Can associate two objects per import file (e.g., Contacts + Companies)
- Good for pilot runs and data validation

**Cons:**
- Cannot import more than two object types per file
- No attachment migration — files must be handled separately
- Loses multi-level relationships (Account → Contact → Case → EmailMessage)
- Salesforce deletes export files after 48 hours or when a new export starts
- No engagement history (emails, calls, meetings)
- No picklist validation during import — unmapped values silently become text strings

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

### 2. API-Based Migration (Salesforce REST/Bulk API → HubSpot CRM API)

**How it works:** Extract data from Salesforce using the REST API or Bulk API 2.0, transform in a custom script (Python, Node.js), and load into HubSpot via the CRM v3/v4 batch APIs. Associations are created in a separate pass using the Associations API.

**When to use it:** Any migration over 50K records, or any migration requiring relationship preservation, engagement history, or attachments.

**Salesforce extraction constraints:**
- Bulk API 2.0 is Salesforce's REST-based API for processing large volumes of data asynchronously — you submit a job, wait for it to process, and retrieve the results.
- You can process up to 100 million records per 24-hour period using Bulk API 2.0. This limit is based on the number of records processed, not the number of jobs or batches created.
- You can upload up to 150 MB of data per job.
- A bulk query can retrieve up to 15 GB of data, divided into 15 files of 1 GB each.
- Bulk API 2.0 query mode does **not** support parent-to-child relationship queries, `GROUP BY`, `OFFSET`, or `TYPEOF`. Using `ORDER BY` or `LIMIT` disables PK chunking.
- **Required Salesforce permissions:** The API user needs "API Enabled" permission, "Bulk API Hard Delete" (if cleaning up), "View All Data" for cross-object extraction, and "Manage Content" for ContentDocument/ContentVersion access. Use a dedicated Integration User profile.

**HubSpot loading constraints:**
- Pro and Enterprise: 100 requests per 10 seconds and 250,000 requests per day (current), with separate limits on Search API (4 requests per second). Upcoming increases: Professional moves to 650,000 requests/day and 190 requests/10 seconds; Enterprise moves to 1,000,000 requests/day and 190 requests/10 seconds.
- Batch read associations: limited to 1,000 inputs per request body. Batch create associations: limited to 2,000 inputs per request body.
- HubSpot batch object endpoints accept up to 100 records per call.
- Search API: capped at 10,000 results per query; 4 requests/second. This is the most common bottleneck because it's the easiest to over-call during validation.

**Throughput benchmarks (at Enterprise rate limits):**
- Ticket creation: 100 records/batch × 19 batches/second (theoretical max) = ~6,840 tickets/minute. Realistic throughput with error handling, logging, and association calls: **2,000–3,000 tickets/minute**.
- Associations: 2,000 pairs/batch. At 19 req/s: ~36,000 associations/minute theoretical, ~15,000/minute realistic.
- Full migration estimate: 500K tickets with Contact + Company associations + email engagements ≈ **18–28 hours of continuous API throughput**, depending on engagement volume and error rates.
- 2M records with associations and engagements: **3–5 days** of continuous API throughput.

**Pros:**
- Full control over transformation logic
- Preserves relationships and engagement history
- Handles attachments via the file upload API
- Can be made idempotent with proper ID mapping

**Cons:**
- Requires developer time (typically 2–4 weeks for mid-market, 4–8 weeks for enterprise)
- Must handle rate limiting, retries, and error logging
- Throughput planning required due to batch size limits

**Complexity:** High | **Scalability:** Enterprise-grade

### 3. Native HubSpot-Salesforce Connector

**How it works:** HubSpot offers a native Salesforce integration that syncs Contacts, Companies, Deals, and Tickets between the two platforms.

**When to use it:** Ongoing bidirectional sync during a parallel-run period. **Not for one-time historical migration.**

The native HubSpot-Salesforce connector is a sync tool, not a migration engine. It keeps two live systems in alignment. When teams repurpose it as a one-time migration path, three constraints break the plan:

1. **Custom object ceiling.** The native integration supports up to 10 custom objects per HubSpot Enterprise account. If your Salesforce org uses more than 10 custom objects — common in mature Service Cloud deployments — the connector cannot sync the rest.
2. **Enterprise requirement.** Custom object sync requires Enterprise-tier licensing in HubSpot; Professional plans have no custom object sync at all.
3. **No historical backfill.** Turning on ticket sync does **not** backfill historical cases. Records only sync when created or updated after the integration is enabled. Historical backfill still needs a separate import.

**Complexity:** Low | **Scalability:** Limited by sync constraints

### 4. Third-Party Migration Tools

Tools like Trujay, Import2, and SyncMatters offer a GUI-based migration path. They handle basic object mapping and can move Contacts, Companies, and Tickets.

**When to use it:** Mid-size migrations (50K–500K records) with standard objects only.

**Typical pricing:** Per-record pricing ranges from $0.01–$0.05/record depending on object type and volume. A 200K-record migration (Contacts + Tickets + Companies) typically runs $3,000–$8,000. Some tools charge flat rates by tier (e.g., Import2's plans start around $500 for small migrations up to $5,000+ for enterprise volumes).

**Pros:**
- Faster setup than custom scripts (hours vs. weeks)
- Pre-built field mapping UI
- Some offer test migrations with rollback

**Cons:**
- Limited control over transformation logic
- Custom objects and complex relationships often unsupported
- Attachment migration varies by tool (ask explicitly)
- Per-record pricing expensive at scale
- Ask for exact handling of cases, comments, files, and idempotent re-runs before committing
- Most tools cannot reconstruct email threading

**Complexity:** Medium | **Scalability:** Mid-market

### 5. Middleware / Integration Platforms (Zapier, Make, Workato)

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

Zapier and Make are trigger-based automation tools. They can move individual records on create/update events but are not designed for bulk historical data migration. A 500K-record migration through Zapier would exhaust task limits (Zapier Professional allows 2,000 tasks/month at the base tier), miss relationships, and take weeks or months.

For enterprise-grade ongoing sync, Workato or a similar iPaaS paired with a managed service for the initial historical load is the more realistic pattern.

**Complexity:** Low–Medium | **Scalability:** Small ongoing sync only (for historical: enterprise iPaaS only)

### 6. Custom ETL Pipeline

**How it works:** Build an explicit extract-transform-load pipeline — often routing Salesforce data into a warehouse (Snowflake, BigQuery) using Fivetran or Airbyte, transforming via dbt, and reverse-ETLing into HubSpot (Census, Hightouch).

**When to use it:** Enterprise environments where the data warehouse is the source of truth, regulated industries, M&A consolidations, or any move with non-standard business logic.

**Pros:**
- Highly resilient, auditable, and repeatable
- Best control over sequencing and rollback
- Natural fit if you already have a modern data stack
- Full audit trail for compliance (HIPAA, SOX, GDPR)

**Cons:**
- Requires existing data infrastructure
- Highest build cost (typically $15K–$50K in engineering time for initial setup) and the most testing
- Overkill for a one-time point-to-point migration without an existing warehouse

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

### Migration Approach Comparison

| Method | Complexity | Best For | Relationships | Attachments | Custom Objects | Typical Cost |
|---|---|---|---|---|---|---|
| CSV Export/Import | Low | < 50K records, simple data | ❌ Limited | ❌ No | ❌ No | $0 (internal time only) |
| API-Based (Custom) | High | Any scale, full fidelity | ✅ Yes | ✅ Yes | ✅ With Enterprise | $10K–$60K in dev time |
| Native Connector | Low | Parallel sync only | ⚠️ Partial | ❌ No | ⚠️ 10 max | $0 (included with HubSpot) |
| Third-Party Tools | Medium | 50K–500K, standard objects | ⚠️ Partial | ⚠️ Varies | ❌ Rarely | $3K–$10K |
| Middleware (Zapier/Make) | Low | Small ongoing sync | ❌ No | ❌ No | ❌ No | $50–$500/month |
| Custom ETL | High | Enterprise data stacks | ✅ Yes | ✅ Yes | ✅ With Enterprise | $15K–$50K+ |

### Which Approach Fits Your Scenario

- **Small business, < 10K records, no custom objects:** CSV export/import. Budget a day for cleanup, a day for import. Accept that historical email threads will be flattened.
- **Mid-market, 10K–500K records, standard objects:** Third-party tool or API scripts if you have engineering bandwidth.
- **Enterprise, 500K+ records, custom objects, attachments:** API-based migration or custom ETL pipeline. This is not optional — it's the only path that preserves full data fidelity.
- **Parallel-run period:** Native connector or middleware for supported standard objects.
- **Regulated industries (HIPAA, SOX, GDPR):** Custom ETL through a warehouse for full audit trail, encryption in transit, and data residency compliance. See the Compliance section below.

## Pre-Migration Planning Checklist

Before touching any API, complete these steps:

1. **Data audit**: Inventory every Salesforce object in scope — Accounts, Contacts, Leads, Cases, Opportunities, Activities (Tasks/Events), EmailMessages, CaseComments, Attachments (ContentDocument/ContentVersion), Knowledge Articles, Entitlements, and all custom objects. Run `SELECT COUNT() FROM [Object]` for each to establish baseline record counts.
2. **Identify dead data**: Drop test records, spam contacts, cases older than your retention policy, and orphaned records with no associations. Enterprise Salesforce instances carry years of custom objects, workflows, and validation rules. Undocumented complexity routinely doubles migration timelines.
3. **Define scope**: Decide which objects migrate, which are archived, and which are dropped. Document the decision. Determine a cutoff date — are you migrating 10 years of closed cases, or just the last 2 years? A common pattern: migrate 2 years of active/recent data, archive the rest to CSV/warehouse.
4. **Map users and agents**: Create all HubSpot owners before importing records. Build a Salesforce User ID → HubSpot Owner ID mapping table. You cannot assign a Ticket to an agent in HubSpot if that agent does not exist yet.
5. **Audit Salesforce permissions**: Ensure your extraction user has "API Enabled," "View All Data," and "Manage Content" permissions. Without "View All Data," Bulk API 2.0 queries will silently return partial results based on the user's sharing rules.
6. **Choose migration strategy**:
   - **Big bang**: All data migrates in a single cutover window. Simpler but higher risk. Typical window: 24–72 hours for mid-market, up to a week for enterprise.
   - **Phased big-bang**: Migrate historical closed data first (Phase 1), then migrate all active cases and open tickets over a weekend and cut over routing rules (Phase 2). Takes the bulk of data volume out of the critical path.
   - **Incremental**: Migrate historical data first, then delta-sync new records until cutover. Best for organizations that cannot afford any downtime.
7. **Risk mitigation**: Take a full Salesforce backup (Data Export Service or Bulk API extract), document rollback procedures, and define success criteria before starting.

## Field-Level Mapping: Salesforce Service Cloud → HubSpot Service Hub

This table covers the most common fields. Your org will have custom fields that require manual mapping.

| Salesforce Field | Salesforce Object | HubSpot Property | HubSpot Object | Transform Required |
|---|---|---|---|---|
| `AccountId` → `Name` | Account | `name` | Company | Lookup domain for dedup |
| `BillingStreet`, `BillingCity`, etc. | Account | `address`, `city`, `state`, `zip` | Company | Split compound address |
| `Industry` | Account | `industry` | Company | Map picklist values |
| `Website` | Account | `domain` | Company | Extract domain; used as HubSpot dedup key |
| `FirstName`, `LastName` | Contact | `firstname`, `lastname` | Contact | Direct |
| `Email` | Contact | `email` | Contact | Direct (primary dedup key) |
| `Phone` | Contact | `phone` | Contact | Direct |
| `AccountId` | Contact | Association to Company | Contact | Requires Company ID mapping |
| `Subject` | Case | `subject` | Ticket | Direct |
| `Status` | Case | `hs_pipeline_stage` | Ticket | Map picklist → pipeline stage ID |
| `Priority` | Case | `hs_ticket_priority` | Ticket | Map picklist values (e.g., "High" → "HIGH") |
| `Origin` | Case | `source_type` | Ticket | Map (Phone → PHONE, Email → EMAIL, Web → WEB) |
| `Description` | Case | `content` | Ticket | Direct; strip unsupported HTML tags |
| `CaseNumber` | Case | Custom property `salesforce_case_number` | Ticket | Store as searchable custom property |
| `RecordTypeId` | Case | `hs_pipeline` | Ticket | Map each RecordType to a HubSpot Pipeline ID |
| `CreatedDate` | Case | `createdate` | Ticket | Convert to Unix ms timestamp |
| `ClosedDate` | Case | `closed_date` | Ticket | Convert to Unix ms timestamp |
| `OwnerId` | Case | `hubspot_owner_id` | Ticket | Map via user ID lookup table |
| `CurrencyIsoCode` | Case / Opportunity | `deal_currency_code` | Deal | HubSpot supports multi-currency on Enterprise only |
| `StageName` | Opportunity | `dealstage` | Deal | Map to Deal pipeline stage IDs |
| `Amount` | Opportunity | `amount` | Deal | Direct; handle currency conversion if multi-currency |
| `CloseDate` | Opportunity | `closedate` | Deal | Convert to Unix ms timestamp |

> [!TIP]
> **Picklist mapping is where data quality breaks.** Salesforce picklists with 30+ values need to be mapped to HubSpot dropdown properties that you create *before* import. Any unmapped value will be rejected by the API with a `400` error: `{"status":"error","message":"Property values were not valid","validationResults": [{"name":"status","error":"INVALID_OPTION"}]}`. Create all dropdown options in HubSpot first, then validate every source value against the target option set programmatically.

## Migration Architecture: Extract → Transform → Load

### Extract Phase

Use Salesforce Bulk API 2.0 for all objects with more than 2,000 records. Any data operation that includes more than 2,000 records is a good candidate for Bulk API 2.0. Do not use the synchronous REST API for extracting 500,000 Cases — you will hit daily API limits and experience severe timeouts.

```python
# Salesforce Bulk API 2.0 Query Job
import requests
import time

SF_INSTANCE = "https://yourorg.my.salesforce.com"
SF_TOKEN = "your_access_token"
API_VERSION = "v60.0"

# Step 1: Create query job
job = requests.post(
    f"{SF_INSTANCE}/services/data/{API_VERSION}/jobs/query",
    headers={"Authorization": f"Bearer {SF_TOKEN}", "Content-Type": "application/json"},
    json={
        "operation": "query",
        "query": "SELECT Id, Subject, Status, Priority, CaseNumber, "
                 "ContactId, AccountId, CreatedDate, ClosedDate, Description, "
                 "OwnerId, Origin, RecordTypeId "
                 "FROM Case WHERE CreatedDate > 2020-01-01T00:00:00Z"
    }
)
job_id = job.json()["id"]

# Step 2: Poll until job completes
while True:
    status = requests.get(
        f"{SF_INSTANCE}/services/data/{API_VERSION}/jobs/query/{job_id}",
        headers={"Authorization": f"Bearer {SF_TOKEN}"}
    ).json()
    if status["state"] in ("JobComplete", "Failed", "Aborted"):
        break
    time.sleep(5)

# Step 3: Download results (CSV, up to 1GB per locator)
results = requests.get(
    f"{SF_INSTANCE}/services/data/{API_VERSION}/jobs/query/{job_id}/results",
    headers={"Authorization": f"Bearer {SF_TOKEN}", "Accept": "text/csv"}
)
# Handle pagination via Sforce-Locator header for results > 1GB
```

Extract objects in dependency order: **Accounts → Contacts → Cases → EmailMessages → CaseComments → Attachments → Activities**. Maintain a mapping table (`salesforce_id → hubspot_id`) for every record. Use separate query jobs for `Case`, `CaseComment`, `EmailMessage`, and file-link objects — Bulk API 2.0 query mode does not support parent-to-child relationship queries.

### Transform Phase

Transformation handles:
- **ID remapping**: Replace Salesforce `AccountId` on Contact records with the HubSpot Company ID from your mapping table
- **Picklist translation**: Convert Salesforce picklist values to HubSpot dropdown internal values (e.g., Salesforce `"Escalated"` → HubSpot pipeline stage ID `"12345678"`)
- **Date formatting**: Salesforce returns ISO 8601 (`2024-01-15T10:30:00.000+0000`); HubSpot accepts Unix millisecond timestamps for date properties (`1705312200000`)
- **Data type coercion**: Salesforce multi-select picklists → HubSpot semicolon-delimited strings (`"ValueA;ValueB;ValueC"`)
- **HTML cleanup**: Strip unsupported HTML from email bodies and descriptions. HubSpot's rich text properties support a subset of HTML — `<script>`, `<style>`, and `<iframe>` tags will cause validation failures
- **Multi-currency handling**: If Salesforce has multi-currency enabled, convert `CurrencyIsoCode` fields to HubSpot's `deal_currency_code`. HubSpot multi-currency requires Enterprise and must be configured before import
- **Deduplication**: Merge duplicate Contacts by email address before import. HubSpot uses email as the unique Contact identifier — pushing duplicate Salesforce records with the same email will cause a `409 Conflict` error: `{"status":"error","message":"Contact already exists. Existing ID: 12345","category":"CONFLICT"}`

### Load Phase

Load into HubSpot in this order:
1. **Companies** (via `POST /crm/v3/objects/companies/batch/create`)
2. **Contacts** (via `POST /crm/v3/objects/contacts/batch/create`)
3. **Deals** (via `POST /crm/v3/objects/deals/batch/create`)
4. **Tickets** (via `POST /crm/v3/objects/tickets/batch/create`)
5. **Associations** (via `POST /crm/v4/associations/{fromObjectType}/{toObjectType}/batch/create`)
6. **Engagements** — emails, notes, calls, meetings
7. **Attachments** — file uploads via the Files API, then associated to records

```python
# HubSpot batch create tickets with rate limiting and error handling
import requests
import time
import json

HUBSPOT_TOKEN = "your_private_app_token"
BATCH_SIZE = 100
BASE_URL = "https://api.hubapi.com"

def create_tickets_batch(tickets, id_mapping, error_log):
    for i in range(0, len(tickets), BATCH_SIZE):
        batch = tickets[i:i + BATCH_SIZE]
        payload = {
            "inputs": [
                {"properties": t["properties"]} for t in batch
            ]
        }
        resp = requests.post(
            f"{BASE_URL}/crm/v3/objects/tickets/batch/create",
            headers={
                "Authorization": f"Bearer {HUBSPOT_TOKEN}",
                "Content-Type": "application/json"
            },
            json=payload
        )

        if resp.status_code == 429:
            # Rate limited — respect Retry-After header
            retry_after = int(resp.headers.get("Retry-After", 10))
            time.sleep(retry_after)
            continue  # Retry this batch

        elif resp.status_code == 207:
            # Partial success — some records failed
            for j, result in enumerate(resp.json().get("results", [])):
                if result.get("status") == "COMPLETE":
                    id_mapping[batch[j]["sf_id"]] = result["id"]
                else:
                    error_log.append({
                        "sf_id": batch[j]["sf_id"],
                        "error": result.get("message"),
                        "category": result.get("category")
                    })

        elif resp.status_code == 201:
            for j, result in enumerate(resp.json().get("results", [])):
                id_mapping[batch[j]["sf_id"]] = result["id"]

        elif resp.status_code == 400:
            # Validation error — log full response for debugging
            error_body = resp.json()
            # Common: INVALID_OPTION, PROPERTY_DOESNT_EXIST, INVALID_EMAIL
            for t in batch:
                error_log.append({
                    "sf_id": t["sf_id"],
                    "error": json.dumps(error_body),
                    "category": "VALIDATION"
                })

        elif resp.status_code == 423:
            # Resource locked — back off significantly
            time.sleep(2)
            continue  # Retry

        else:
            for t in batch:
                error_log.append({
                    "sf_id": t["sf_id"],
                    "error": resp.text,
                    "category": f"HTTP_{resp.status_code}"
                })

        time.sleep(0.5)  # Respect burst limits (~2 requests/second conservative)

    return id_mapping, error_log
```

### Common HubSpot API Error Responses During Migration

| HTTP Status | Error Category | Example Response | Resolution |
|---|---|---|---|
| 400 | `INVALID_OPTION` | `{"message":"Property values were not valid","validationResults": [{"name":"hs_pipeline_stage","error":"INVALID_OPTION","value":"Escalated"}]}` | Create the missing dropdown option in HubSpot before import, or fix the picklist mapping |
| 400 | `PROPERTY_DOESNT_EXIST` | `{"message":"Property values were not valid","validationResults": [{"name":"custom_field__c","error":"PROPERTY_DOESNT_EXIST"}]}` | Create the custom property in HubSpot before import |
| 409 | `CONFLICT` | `{"status":"error","message":"Contact already exists. Existing ID: 551234","category":"CONFLICT"}` | Deduplicate contacts by email before pushing, or use upsert endpoint |
| 429 | Rate limit | `{"status":"error","message":"You have reached your secondly limit","category":"RATE_LIMITS"}` | Respect `Retry-After` header; implement token bucket rate limiter |
| 423 | `LOCKED` | `{"status":"error","message":"Resource is locked","category":"LOCKED"}` | Wait 2+ seconds and retry; occurs during heavy concurrent writes |

> [!NOTE]
> **Rate limit strategy matters.** HubSpot uses a token bucket model for rate limiting. A batch read of 100 contacts costs 1 request, not 100. Always use batch endpoints. A migration of 500K tickets at 100 per batch = 5,000 API calls for records alone, plus ~250,000 association calls (at 2,000/batch = 125 calls per association type), plus engagement calls. At Enterprise limits (1M requests/day), a 500K-ticket migration with associations and engagements consumes roughly 15–25% of your daily quota per day over a multi-day window. Budget accordingly and avoid running other integrations during the migration window.

## Step-by-Step Migration Process

1. **Prepare HubSpot**: Create all custom properties, pipeline stages, ticket statuses, and custom objects before importing any data. Map Salesforce picklist values to HubSpot dropdown options. Verify all pipeline stage IDs via the Pipelines API.
2. **Create users/owners**: Set up all HubSpot owners. Build a User ID mapping table (Salesforce `UserId` → HubSpot `ownerId`).
3. **Export from Salesforce**: Run Bulk API 2.0 query jobs for each object. Store results as CSV or JSON. Verify record counts match expected totals.
4. **Transform**: Clean data, remap IDs, translate picklists, deduplicate contacts, handle nulls and data type mismatches. Run validation checks on the transformed payload before loading.
5. **Load Companies**: Batch create, store mapping table.
6. **Load Contacts**: Batch create, associate to Companies.
7. **Load Deals**: Batch create, associate to Companies and Contacts.
8. **Load Tickets**: Batch create, associate to Contacts and Companies. Map Salesforce `RecordTypeId` to HubSpot Pipeline and `Status` to Pipeline Stage.
9. **Load Engagements**: Create email, note, call, and meeting engagements. Associate each to its parent Ticket and Contact. Preserve original timestamps.
10. **Load Attachments**: Upload files via the Files API, then associate to the correct records.
11. **Validate**: Run record counts, field-level spot checks, and association integrity checks (see Validation section).

## Edge Cases and Challenges

### Duplicate Records

Salesforce allows duplicate Contacts under different Accounts. HubSpot deduplicates Contacts by email address. If two Salesforce Contacts share an email but belong to different Accounts, HubSpot will merge them into one Contact — and you'll lose one Account association unless you use multi-company associations (available on all tiers). Deduplicate your payload in memory before pushing. Strategy: identify duplicates, choose the "primary" record, and merge all Case/Ticket associations onto that single Contact with multiple Company associations.

### Multi-Level Relationships

Account → Contact → Case → EmailMessage → Attachment is a four-level chain. Each level must be imported in order, and each mapping table must be populated before the next level can be loaded. One broken link at any level cascades into orphaned records downstream.

Salesforce allows a Case to be linked to an Account without a Contact, or to multiple Contacts. HubSpot requires explicit associations via the Associations API — you must define primary associations (`Ticket → Contact`, `Ticket → Company`) for each record. For Cases without a Contact in Salesforce, create the Ticket-to-Company association only and flag these records for post-migration review.

### Email Threading and Conversation Reconstruction

Salesforce stores email threading metadata on `EmailMessage` records via `ThreadIdentifier` and `ParentId` fields. `ThreadIdentifier` groups emails in the same thread; `ParentId` links a reply to its parent email.

HubSpot's email engagements are flat — there is no native parent-child threading for engagement objects. To preserve threading:

1. Extract all `EmailMessage` records with `ThreadIdentifier`, `ParentId`, `MessageDate`, `FromAddress`, `ToAddress`, `Subject`, `TextBody`/`HtmlBody`
2. Group by `ThreadIdentifier` to identify conversation threads
3. Create email engagements in chronological order on the Ticket timeline, preserving `MessageDate` as the engagement timestamp
4. If using HubSpot's Conversations API instead of the Engagements API, you can create a conversation thread and add messages to it — this preserves visual threading in the HubSpot inbox but requires Conversations Inbox configuration
5. Include `In-Reply-To` and `References` email headers (if stored in Salesforce's `Headers` field) in the engagement metadata for compliance purposes

This is one of the most common migration failure points. Flat email engagements on a timeline are functional but lose the visual threading that agents relied on in Salesforce. Set expectations with the support team.

### Attachments and Files

Salesforce stores files in ContentDocument/ContentVersion objects. Extraction requires querying `ContentDocumentLink` to find which records files are attached to, then downloading the binary via the REST API. To migrate these:

1. Query `ContentDocumentLink` filtered by `LinkedEntityId` (the Case/Account/Contact ID)
2. For each `ContentDocumentId`, query `ContentVersion` to get `VersionData` (the binary endpoint), `Title`, `FileExtension`, and `ContentSize`
3. Download the binary file via `GET /services/data/v60.0/sobjects/ContentVersion/{id}/VersionData`
4. Upload to HubSpot's File Manager API: `POST /filemanager/api/v3/files/upload`
5. Associate the file to the Ticket using the `hs_attachment_ids` property, or create an Engagement Note with the hosted file URL embedded

**Size limits:** HubSpot's File Manager accepts files up to 300MB each. Salesforce ContentVersion files up to 2GB are possible — files exceeding HubSpot's limit must be hosted externally and linked.

### Knowledge Base Articles

HubSpot does not offer a bulk import API for Knowledge Base articles. Articles must be created individually via the CMS API or manually recreated. If you have hundreds of articles with embedded images and cross-links:

- Budget 2–5 minutes per article for manual recreation, or
- Use the CMS Blog/Pages API to create articles programmatically (one at a time, not batched), or
- Export articles as HTML/Markdown, write a script to POST each via the API, then manually review formatting

Cross-links between articles must be updated to use HubSpot KB URLs after migration. Embedded images must be re-uploaded to HubSpot's file manager and their `src` URLs replaced.

### CaseComments vs. EmailMessages

Salesforce treats CaseComments (internal notes) and EmailMessages (email correspondence) as separate objects. In HubSpot, CaseComments map to Note engagements and EmailMessages map to Email engagements. Both associate to the Ticket timeline, but they're different engagement types. Don't conflate them. Preserve the original author (`CreatedById`/`FromName`) and timestamp (`CreatedDate`/`MessageDate`) in the note body metadata, since HubSpot Note engagements don't have native "author" fields that map to arbitrary users — only the `ownerId` (which represents the HubSpot user who "created" the engagement).

### Entitlements, Milestones, and SLAs

Salesforce's Entitlement and Milestone objects have no HubSpot equivalent. If your support operation relies on contractual SLA tracking:

- **Option A:** Store Entitlement data as custom properties on Company or Ticket records (e.g., `sla_tier`, `response_time_target_hours`, `resolution_time_target_hours`). Use HubSpot Workflows to trigger escalation notifications based on these values. This is a workaround, not a replacement.
- **Option B:** Maintain Entitlement tracking in a separate system (a database, spreadsheet, or purpose-built SLA tool) and reference it from HubSpot via custom properties containing external record IDs.
- **Option C:** Accept the capability gap and simplify to HubSpot's native SLA tool (3 priority-based policies on Professional+). Document what's lost.

HubSpot's SLA statuses are only visible within HubSpot's Conversations function. Tickets created via API, form, or manual entry outside of Conversations do not trigger SLA tracking unless connected to a conversation thread.

### Multi-Currency and Multi-Language

**Multi-currency:** If Salesforce has multi-currency enabled, each Opportunity/Case may have a `CurrencyIsoCode` field. HubSpot supports multi-currency on Enterprise only. Enable it in HubSpot Settings → Account Defaults → Currency before importing Deals. Map `CurrencyIsoCode` to `deal_currency_code`. If targeting HubSpot Professional, you must convert all amounts to a single currency during transformation.

**Multi-language Knowledge Base:** Salesforce Knowledge supports article translations via `KnowledgeArticleVersion` with language variants. HubSpot Knowledge Base supports multi-language on Professional+ but requires creating separate articles per language with language tags. There is no automatic translation linking — you must manually associate translated articles or use a naming convention.

### API Failures and Retries

Don't retry failed requests instantly. Add exponential backoff with jitter:

```python
import random
import time

def retry_with_backoff(func, max_retries=5):
    for attempt in range(max_retries):
        result = func()
        if result.status_code == 429:
            base_delay = min(2 ** attempt, 60)  # Cap at 60 seconds
            jitter = random.uniform(0, base_delay * 0.5)
            time.sleep(base_delay + jitter)
        elif result.status_code == 423:
            time.sleep(2 + random.uniform(0, 1))
        elif result.status_code >= 500:
            time.sleep(5 * (attempt + 1))
        else:
            return result
    raise Exception(f"Max retries exceeded after {max_retries} attempts")
```

Log every failed record with its Salesforce ID, the error response body, and the request payload. After the migration, retry all failures in a separate pass. Don't retry validation errors (400) until the transform layer is fixed — retrying bad data just burns API quota.

## Compliance and Audit Requirements

For regulated industries (HIPAA, SOX, GDPR, CCPA), migration requires additional safeguards:

**Data residency:** HubSpot's data hosting is US-based by default (AWS us-east-1). EU data hosting is available on Enterprise plans and must be requested during provisioning. If your Salesforce org stores EU customer data under GDPR, confirm HubSpot data residency before migration. Data in transit during ETL must be encrypted (TLS 1.2+ for all API calls — both Salesforce and HubSpot APIs enforce this by default).

**PHI/PII handling during ETL:** If migrating healthcare data subject to HIPAA, HubSpot requires a BAA (Business Associate Agreement) which is available on Enterprise plans. During the ETL process, ensure that intermediate storage (local files, staging databases, warehouse tables) is encrypted at rest and access-controlled. Never log PHI in plaintext error messages.

**Audit trail:** Log every record migration with source ID, destination ID, timestamp, and user/service account. Store logs in an immutable or append-only system. For SOX compliance, maintain evidence that migrated financial data (Deal amounts, close dates) matches source records exactly. The `salesforce_id ↔ hubspot_id` mapping table serves as the primary audit artifact.

**Data retention:** If your Salesforce data retention policy requires deletion of records after migration, document the deletion timeline and get sign-off. Keep Salesforce active for at least 90 days post-migration in regulated environments (vs. 30 days standard).

## Limitations and Constraints Summary

| Constraint | Detail |
|---|---|
| Custom object ceiling | 10 custom object definitions per HubSpot Enterprise account; 500K records each |
| No Entitlement/Milestone objects | Must flatten to custom properties or external system |
| No CaseComment object | Map to Note engagements; lose structured parent-child relationship |
| Record Types → Pipelines | Use HubSpot Pipelines to differentiate support processes |
| API throughput ceiling | 190 req/10s, 1M req/day (Enterprise). 500K tickets + associations ≈ 18–28 hours |
| No bulk KB import | Each Knowledge Base article created individually |
| Search API cap | 10,000 results per query; use object reads for validation |
| SLA policy limit | 3 priority-based SLA policies on Professional+ |
| Multi-currency | Enterprise only; Professional requires single-currency conversion |
| Attribution history | Multi-touch attribution from Salesforce campaigns does not transfer; budget 12–36 months of lost revenue proof points |
| HubSpot sandbox limits | Sandboxes don't support all object types; test migrations may not fully replicate production behavior |

## Validation and Testing

### Record Count Comparison

After migration, compare counts per object:

| Object | Salesforce Count | HubSpot Count | Delta | Status |
|---|---|---|---|---|
| Accounts → Companies | `SELECT COUNT() FROM Account` | Companies API total | ≤ 0.1% | ✅ / ❌ |
| Cases → Tickets | `SELECT COUNT() FROM Case WHERE CreatedDate > cutoff` | Tickets API total | ≤ 0.1% | ✅ / ❌ |
| Contacts | `SELECT COUNT() FROM Contact` | Contacts API total | Check for dedup merges | ✅ / ❌ |
| Associations | Count of Case-Contact links | Count of Ticket-Contact associations | Must match | ✅ / ❌ |

A count mismatch of > 0.1% warrants investigation before go-live.

### Field-Level Validation

Sample 50–100 records per object type using stratified sampling (include oldest, newest, highest-value, most-associated records). Verify:
- All mapped fields contain correct values
- Picklist values translated correctly
- Dates are accurate (timezone issues are common — Salesforce stores in UTC, verify HubSpot interpretation)
- Owner assignments are correct
- `salesforce_case_number` or `legacy_salesforce_id` populated on every record

### Association Integrity

For every Ticket, verify it's associated to the correct Contact and Company. For every email engagement, verify it's associated to the correct Ticket. Broken associations are the #1 post-migration support complaint.

Automate this check:
```python
# Verify ticket associations
def validate_associations(id_mapping, hubspot_token):
    errors = []
    for sf_case_id, hs_ticket_id in id_mapping.items():
        resp = requests.get(
            f"https://api.hubapi.com/crm/v4/objects/tickets/{hs_ticket_id}/associations/contacts",
            headers={"Authorization": f"Bearer {hubspot_token}"}
        )
        actual_contacts = {r["toObjectId"] for r in resp.json().get("results", [])}
        expected_contacts = get_expected_contacts(sf_case_id, id_mapping)
        if actual_contacts != expected_contacts:
            errors.append({"ticket": hs_ticket_id, "expected": expected_contacts, "actual": actual_contacts})
    return errors
```

### UAT Process

Give 3–5 support agents access to the migrated data in a HubSpot sandbox (if available) or a test pipeline. Have them pull up real customer histories and verify completeness. Use stratified sampling across old/new cases, high-value accounts, and attachment-heavy tickets. Agents will catch issues that automated checks miss — particularly around email readability, note formatting, and timeline ordering.

### Rollback Plan

Keep your Salesforce org active and unchanged for at least 30 days post-migration (90 days for regulated industries). Always store the Salesforce ID in a custom property in HubSpot (e.g., `legacy_salesforce_id`). Rollback procedure:

1. Stop all HubSpot inbound routing (pause Conversations Inbox, disable form-to-ticket workflows)
2. Reroute support email addresses back to Salesforce Email-to-Case
3. Use the `legacy_salesforce_id` property to identify all migrated records
4. Batch-delete migrated records via the HubSpot batch archive endpoint (`POST /crm/v3/objects/{objectType}/batch/archive`)
5. For records created in HubSpot during the parallel-run period (not in Salesforce), export via HubSpot API before deletion and manually create in Salesforce
6. Verify Salesforce routing rules and automations are still active
7. Communicate rollback to the support team with updated procedures

## Post-Migration Tasks

Once the data is in HubSpot Service Hub, the operational work begins:

1. **Rebuild automations**: Salesforce Workflow Rules, Process Builder flows, and Flow automations do not export. Audit every automation in Salesforce (Setup → Flows, Setup → Process Builder, Setup → Workflow Rules), document the trigger conditions, criteria, and actions, and rebuild each in HubSpot Workflows. Common translations: Salesforce Process Builder → HubSpot Workflow; Salesforce Assignment Rules → HubSpot round-robin + conditional routing; Salesforce Escalation Rules → HubSpot SLA + Workflow escalation.
2. **Rebuild reports and dashboards**: Salesforce reports don't transfer. Recreate your key dashboards in HubSpot's reporting tools. HubSpot's custom report builder supports cross-object reporting on Professional+.
3. **Reconfigure Email-to-Ticket**: Reroute your support email addresses (e.g., support@company.com) from Salesforce Email-to-Case to HubSpot's Conversations Inbox. Update MX records or email forwarding rules. Test round-trip: send a test email → verify ticket creation → verify agent can reply from HubSpot.
4. **Configure SLAs**: Set up HubSpot SLA goals in the help desk. Understand the limitations: 3 priority-based policies maximum, no milestone-level tracking, SLAs only apply to Conversations-connected tickets.
5. **Train agents**: HubSpot's timeline-based UI is fundamentally different from Salesforce's tab-based interface. Budget 1–2 weeks for agent training on the help desk workspace, ticket queues, knowledge base, and Conversations inbox. Create a cheat sheet mapping Salesforce actions to HubSpot equivalents.
6. **Monitor for 30 days**: Watch for data inconsistencies, broken associations, missing engagements, and user adoption issues. Assign a DRI (directly responsible individual) for post-migration issues. Track: ticket resolution time, agent response time, and escalation rates vs. pre-migration baselines.

## Estimated Migration Timelines

| Scenario | Records | Objects | Estimated Duration | Engineering Effort |
|---|---|---|---|---|
| Small business (CSV) | < 10K | Contacts, Companies, Tickets | 1–3 days | 0.5 FTE × 1 week |
| Mid-market (API or tool) | 10K–100K | Standard objects + basic engagements | 2–4 weeks | 1 FTE × 3 weeks |
| Mid-market complex (API) | 100K–500K | Standard + some custom objects + attachments | 4–6 weeks | 1–2 FTE × 5 weeks |
| Enterprise (API/ETL) | 500K–5M | Full schema + custom objects + email history + attachments | 6–12 weeks | 2–3 FTE × 8 weeks |
| Enterprise regulated (ETL) | Any | Full schema + compliance requirements | 8–16 weeks | 2–4 FTE × 12 weeks |

These estimates include planning, development, testing, UAT, and cutover. They do not include post-migration automation rebuilding or agent training.

## When to Use a Managed Migration Service

Build in-house when you have:
- A dedicated developer with Salesforce and HubSpot API experience
- Fewer than 100K total records
- No custom objects
- Standard field mappings with minimal transformation

Use a managed service when:
- Your org has 10+ custom objects
- You need attachment migration at scale
- You have complex multi-level relationships (Account → Contact → Case → EmailMessage → Attachment)
- You have multi-threaded email conversations stored as Salesforce EmailMessages that need to render correctly in HubSpot's timeline
- Your team can't afford 4–8 weeks of developer time on a migration
- You need zero-downtime cutover with delta sync
- You're in a regulated industry requiring audit trails and compliance documentation

Building an API-based migration in-house often looks like a two-week project on a Jira board. In reality, undocumented API behaviors, corrupted historical data, rate-limit throttling, and edge cases (duplicate contacts, orphaned records, files exceeding size limits) turn it into a multi-month effort.

Platform migrations break things. Data gets lost. Automations stop working. Reports stop matching reality. Deals get missed because routing fails. Scope the risk honestly.

At ClonePartner, we've completed 1,500+ data migrations, including complex Salesforce Service Cloud projects with custom objects, multi-level associations, and large attachment volumes. We handle the ETL pipeline, field mapping, rate limit management, and validation — so your engineering team stays focused on product work. If your migration involves more than basic Contacts and Tickets, [talk to us](https://cal.com/clonepartner/meet?duration=30).

## Best Practices

- **Back up everything** before migration. Run a full Salesforce Data Export (Setup → Data Export) and store the ZIP files offline. Also run a Bulk API 2.0 extract of every in-scope object as a separate backup.
- **Run a test migration first.** Push a 1–5% sample of your data into a HubSpot sandbox. Validate field mapping, picklist translation, and associations before running the full migration. Use real edge cases (high-attachment tickets, multi-Contact cases, multi-currency deals), not toy data.
- **Import in dependency order.** Companies → Contacts → Deals → Tickets → Associations → Engagements → Attachments. Breaking this order creates orphaned records.
- **Use batch endpoints everywhere.** A batch read of 100 contacts costs 1 request, not 100. Refactor any loop that reads single records.
- **Log everything.** Every API call, every error, every retry. Store logs with Salesforce ID, HubSpot ID, timestamp, HTTP status, and response body. You'll need these logs during validation and any post-migration fixes.
- **Map IDs bidirectionally.** Maintain a persistent `salesforce_id ↔ hubspot_id` mapping table. Keep it under version control or in a database with row-level timestamps.
- **Automate validation.** Write scripts to compare record counts, spot-check field values, and verify associations — don't rely on manual checks for 500K+ records.
- **Use incremental validation.** Validate after each load pass instead of waiting for the end.
- **Freeze Salesforce changes during cutover.** Prevent new Cases from being created in Salesforce during the final migration window to avoid data drift. Use a Salesforce Validation Rule to block new Case creation during the cutover period.
- **Document everything.** Maintain a migration runbook with: object scope, field mappings, picklist translations, user mappings, cutover timeline, rollback procedures, and success criteria.

## Automation Script Outline (Python)

High-level structure for a production API migration script:

```python
# salesforce_to_hubspot_migration.py

class SalesforceExtractor:
    """Handles Bulk API 2.0 query jobs for each object type."""
    def extract_object(self, soql_query: str) -> list[dict]: ...
    def download_attachments(self, content_doc_ids: list) -> list: ...
    def poll_job_status(self, job_id: str) -> str: ...

class DataTransformer:
    """Cleans, remaps, and translates extracted Salesforce data."""
    def map_picklists(self, records, mapping_config) -> list[dict]: ...
    def remap_ids(self, records, id_map) -> list[dict]: ...
    def deduplicate_contacts(self, contacts) -> list[dict]: ...
    def convert_dates_to_unix_ms(self, records, date_fields) -> list[dict]: ...
    def validate_required_fields(self, records, schema) -> tuple[list, list]: ...

class HubSpotLoader:
    """Batch-creates records and associations via HubSpot CRM API."""
    def batch_create(self, object_type, records, batch_size=100) -> dict: ...
    def batch_associate(self, from_type, to_type, pairs) -> None: ...
    def upload_file(self, file_bytes, filename) -> str: ...
    def batch_create_engagements(self, engagement_type, records) -> dict: ...

class MigrationOrchestrator:
    """Runs the full pipeline in dependency order with logging."""
    def run(self):
        # 1. Extract all objects from Salesforce (dependency order)
        # 2. Transform and build mapping tables
        # 3. Load into HubSpot: Companies → Contacts → Deals → Tickets
        # 4. Create associations (Ticket→Contact, Ticket→Company, etc.)
        # 5. Load engagements (emails, notes, calls, meetings)
        # 6. Upload and associate attachments
        # 7. Validate counts and spot-check fields
        # 8. Generate migration report with success/failure counts

class RateLimiter:
    """Token bucket implementation respecting HubSpot burst limits."""
    def __init__(self, requests_per_second: float = 15, burst: int = 190): ...
    def wait_if_needed(self) -> None: ...

class MigrationLogger:
    """Logs every API call, error, and retry for audit trail."""
    def log_success(self, sf_id, hs_id, object_type, timestamp) -> None: ...
    def log_failure(self, sf_id, error, object_type, request_payload) -> None: ...
    def generate_report(self) -> dict: ...
```

This is a structural outline, not production code. Each class handles one responsibility. The orchestrator controls execution order. The rate limiter prevents 429s. The logger creates the audit trail you'll need during validation and compliance review.

## Making the Right Call

A Salesforce Service Cloud to HubSpot Service Hub migration is technically straightforward for small, simple orgs — and genuinely complex for enterprise deployments with custom objects, entitlements, multi-level relationships, email threading, and years of history. The data model gap between the two platforms means you're not just moving records; you're translating one system's architecture into another's.

Get the object mapping right. Extract in the correct order. Respect the API limits. Handle email threading explicitly. Validate ruthlessly. And if the scope exceeds what your team can deliver in a reasonable timeline, bring in a team that's done it before.

> Need help migrating from Salesforce Service Cloud to HubSpot Service Hub? ClonePartner handles the full pipeline — extraction, transformation, loading, and validation — with zero downtime and guaranteed data accuracy. Book a 30-minute call with our migration engineers.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I use the native HubSpot-Salesforce connector to migrate historical data?

No. The native connector is a sync tool, not a migration engine. It supports up to 10 custom objects (Enterprise only), cannot migrate attachments or full engagement history, and does not backfill historical cases — records only sync when created or updated after the integration is enabled. Use the Salesforce Bulk API and HubSpot CRM API for one-time migration.

### How long does a Salesforce Service Cloud to HubSpot Service Hub migration take?

Small orgs (under 50K records, no custom objects) can complete a CSV-based migration in 1–3 days. Mid-market orgs (50K–500K records) typically need 2–4 weeks with API-based migration. Enterprise orgs with custom objects, attachments, and complex relationships should plan for 4–8 weeks.

### Do I need HubSpot Enterprise to migrate Salesforce custom objects?

Yes. HubSpot custom objects are gated to the Enterprise tier across all Hubs. Enterprise allows up to 10 custom object definitions with 500,000 records each. If your Salesforce org has more than 10 custom objects, you'll need to consolidate, flatten into custom properties, or drop some during migration.

### What Salesforce Service Cloud data cannot be migrated to HubSpot?

Entitlements, Milestones, Service Contracts, and Assets have no native HubSpot equivalent. Salesforce Workflows, Process Builder flows, and validation rules cannot be exported — they must be rebuilt manually in HubSpot. Multi-touch attribution data from Salesforce Campaigns also does not transfer without custom mapping.

### What are the HubSpot API rate limits for migration?

HubSpot Professional allows up to 650,000 requests/day with a 190 requests/10-second burst limit. Enterprise allows 1,000,000 requests/day with the same burst limit. The Search API has a separate 4 requests/second limit. Use batch endpoints (100 records per call) to maximize throughput.
