> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbitforms.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating from HubSpot

> Move a HubSpot portal into Orbit: the built-in importer, the script for portals that need transforming on the way, and how every HubSpot object and engagement field maps to its Orbit counterpart.

> [!TIP]
>
> Most teams need no code. **Settings → Integrations → HubSpot → Import from HubSpot** pulls contacts, companies, deals, pipelines, custom properties, engagements and opt-outs straight from HubSpot in the background, resumably, with HubSpot ids kept. [How it works](https://docs.orbitforms.ai/integrations/hubspot.md#import-from-hubspot). This page is for running the same move from code, when you want to transform data on the way or bring attachments across.

HubSpot keeps a record's history as *engagements*: notes, emails, calls, meetings and tasks, each with a timestamp, an owner and the records it is associated with, plus attachments in its Files tool. Orbit keeps the same things: notes and tasks, logged activities for emails, calls, meetings and messages, and files on records. This page maps one to the other.

## The script

`scripts/maintenance/migrate-from-hubspot.ts` in the Orbit repository walks a portal's companies, contacts, deals, engagements and attachments into Orbit through the [CRM API](https://docs.orbitforms.ai/developers/api/crm.md), in batches, with a HubSpot private app token and an Orbit API key (the **CRM agent** preset plus `activities:write` and `files:write`). It is safe to re-run: every row carries its HubSpot id as `external_id` under the source `hubspot`, so a second pass updates instead of duplicating.

```bash theme={null}
HUBSPOT_TOKEN=pat-... ORBIT_API_KEY=sk_... npx tsx scripts/maintenance/migrate-from-hubspot.ts --since 2023-01-01
```

It follows the order in [Migrating from another CRM](https://docs.orbitforms.ai/developers/migrating-from-another-crm.md): members, structure, companies, contacts, deals, history, files, do-not-contact. `--dry-run` reads HubSpot and reports what would be written, `--only` runs a subset of the phases and `--limit` caps each phase for a trial run.

## Field mapping

What the script sends, and what any other code reading HubSpot's API should send. Timestamps from HubSpot are Unix milliseconds; Orbit accepts them as they are or as ISO 8601.

### Every engagement

| HubSpot | Orbit | Notes |
| - | - | - |
| object id | `external_id` (with `external_source: "hubspot"`) | Makes re-runs idempotent and lets a run be undone with `POST /api/v1/activity-imports/{id}/undo` |
| `hs_timestamp` | `occurred_at` (`created_at` for notes and tasks) | Its place on the timeline |
| `hubspot_owner_id` → owner email | `actor_email` (`author_name` on notes and tasks for non-members) | Matched to a member by email; otherwise kept as text |
| associations → contact / company / deal ids | `contact_external_id`, `company_external_id`, `deal_external_id` | Resolved through the ids stored when the records were imported |
| `hs_attachment_ids` | `POST /api/v1/files/import` with `activity_id` or `note_id` | After the row is imported, using HubSpot's signed URL for each file |

### Notes

| HubSpot | Orbit |
| - | - |
| `hs_note_body` (HTML, up to 65,536 characters) | `body` (HTML is flattened to text) |

### Emails

| HubSpot | Orbit |
| - | - |
| `hs_email_direction` `EMAIL` / `INCOMING_EMAIL` / `FORWARDED_EMAIL` | `direction` `outbound` / `inbound` / `inbound` |
| `hs_email_subject` | `title` |
| `hs_email_html`, `hs_email_text` | `body_html` (sanitized), `body` |
| `hs_email_status` `SENT` / `BOUNCED` / `FAILED` / `SCHEDULED` / `SENDING` | `status` `sent` / `bounced` / `failed` / `sent` / `sent` (use `replied` or `received` when you know) |
| `hs_email_headers` `from`, `to`, `cc`, `bcc` (`email`, `firstName`, `lastName`) | `participants` `from`, `to`, `cc`, `bcc` (`email`, `name`) |

### Calls

| HubSpot | Orbit |
| - | - |
| `hs_call_direction` `INBOUND` / `OUTBOUND` | `direction` |
| `hs_call_title`, `hs_call_body` | `title`, `body` |
| `hs_call_duration` (milliseconds) | `duration_seconds` (divide by 1,000) |
| `hs_call_disposition` Connected / No answer / Busy / Left voicemail / Wrong number | `outcome` `connected` / `no_answer` / `busy` / `voicemail_left` / `wrong_number` |
| `hs_call_from_number`, `hs_call_to_number` | `participants.from_number`, `participants.to_number` |
| `hs_call_recording_url` | `external_url` (or fetch the recording with `POST /api/v1/files/import`) |

### Meetings

| HubSpot | Orbit |
| - | - |
| `hs_meeting_title`, `hs_meeting_body` | `title`, `body` |
| `hs_internal_meeting_notes` | append to `body` |
| `hs_meeting_start_time`, `hs_meeting_end_time` | `occurred_at`, `ended_at` |
| `hs_meeting_outcome` `SCHEDULED` / `COMPLETED` / `RESCHEDULED` / `NO_SHOW` / `CANCELED` | `outcome` (same values; `cancelled` is accepted either way) |
| `hs_meeting_location` | `location` |
| `hs_meeting_external_url` | `external_url` |
| attendee associations | `participants.attendees` |

### Tasks

| HubSpot | Orbit |
| - | - |
| `hs_task_subject`, `hs_task_body` | `title`, `description` |
| `hs_timestamp` (due) | `due_at` |
| `hs_task_status` `COMPLETED` | `status: "done"` with `completed_at` |
| `hs_task_priority` `LOW` / `MEDIUM` / `HIGH` | `priority` `low` / `normal` / `high` |
| owner | `assignee_email` (a non-member leaves the task unassigned) |

### Communications (SMS, WhatsApp, LinkedIn)

| HubSpot | Orbit |
| - | - |
| `hs_communication_channel_type` `SMS` | `type: "sms"` |
| any other channel | `type: "message"` |
| `hs_communication_body` | `body` |

## Check the result

Open a few contacts in Orbit: the timeline shows the migrated emails, calls, meetings and notes at the dates they happened, each marked with who did it and that it came from HubSpot. `GET /api/v1/timeline` reads the same feed. If a run went wrong, `POST /api/v1/activity-imports/{id}/undo` removes everything that activity import created; contacts have their own undo on the contact import.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.