Skip to main content
Orbit’s API is built for a migration of any size: every object has a bulk import that takes up to 1,000 rows per call, rows carry the ids and dates they had in the source system, and a second run updates instead of duplicating. A 50,000-contact HubSpot portal with its companies, deals and history is a few hundred calls and well under an hour.
No code needed for most moves: Settings → Migrate CRM in the app pulls a HubSpot portal directly, and takes companies, contacts and deals as CSV sheets from any other CRM. This page is for scripts that drive the move through the API.

The order

Load records before the things that point at them, so every link resolves on first pass.
1

Members

Invite the people who own records in the old CRM. Owners are matched by email (owner_email); an owner who is not a member leaves the record unowned, and a note or task keeps the author’s name as text.
2

Structure

Pipelines and stages with POST /api/v1/pipelines (stage kind decides open, won or lost, with a probability), custom fields with POST /api/v1/crm/fields. GET /api/v1/crm/schema shows what exists. The imports can also create text fields on the fly with create_missing_fields: true.
3

Companies

POST /api/v1/companies/import, 1,000 rows per call, with domain and the source id as external_id.
4

Contacts

POST /api/v1/contacts/import, 1,000 rows per call. company (the name) links each contact to the company imported above; created_at keeps the original creation date.
5

Deals

POST /api/v1/deals/import, 1,000 rows per call, naming the pipeline and stage by name, the company by company_external_id and the contacts by contact_external_ids. A closed deal keeps its closed_at.
6

History

POST /api/v1/activities/import, 500 rows per call: notes, emails, calls, meetings and tasks with their original occurred_at, attached by contact_external_id, company_external_id and deal_external_id.
7

Do-not-contact

POST /api/v1/sequences/unsubscribes with up to 1,000 emails per call for everyone who opted out or bounced in the old system, so nobody is emailed by mistake.

Ids and dates

Send external_source once per call (for example "hubspot") and each row’s external_id. The id is stored with the record, and from then on any call can name the record by it: company_external_id on a deal, deal_external_id on an activity, contact_external_id anywhere a contact is linked. A row whose id is already known updates the record (companies also match on domain), so a migration can stop, resume and re-run safely; duplicate_mode: "skip" keeps what is already there. created_at on contacts, companies, deals, notes and tasks, occurred_at on activities, completed_at on tasks, and closed_at and stage_entered_at on deals keep the original timestamps, so timelines and reports read the way they did before. stage_entered_at is when a new deal entered its current stage: time in stage and the deal’s stage history start there (without it, at closed_at for a closed deal, else created_at). A row that matches a record Orbit already had (a form lead, a company created from an email domain) moves its created_at back to the row’s when the row’s is earlier, never forward.
Every import answers with counts, the row-level errors it left out, warnings for rows that imported with something missing (a contact that was never imported, say), and records: the Orbit id of each row.

Limits

A 429 carries Retry-After in seconds. An approved OAuth app has higher request limits (see rate limits).

Webhooks, apps and workflows during a migration

Nothing you import fires anything by default:
  • Workflows never start from an import unless the person importing picks one in the import dialog (“Contact Added” trigger). The API’s bulk endpoints and CRM migrations start none.
  • Webhooks and connected apps do not receive the records an import writes. The workspace’s owner can turn that on under Settings → Integrations → Connected apps → Send imported records to integrations; it is an explicit, recorded acceptance, applies to every integration in the workspace, and only covers imports that run after it. Deliveries for imported records then carry "source": "import" so a receiver can tell them apart. See Imports.
So the safe order is: connect the apps and webhooks you want, run the migration, and leave the import setting off unless an integration genuinely needs every historical record pushed to it.

What does not carry over

Form definitions, email templates, sequences and workflows are built in Orbit rather than imported; files can be attached with POST /api/v1/files/import from a signed URL in the old system. Migrating from HubSpot maps every HubSpot object and engagement field to its Orbit counterpart.