> ## 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 another CRM

> Bring a whole CRM into Orbit over the API: the order to load things in, batch sizes, how ids and dates carry over, and what the limits are.

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.

> [!TIP]
>
> Coming from HubSpot? Orbit can pull everything itself from **Settings → Integrations → HubSpot → Import from HubSpot**, with no API work at all. This page is for scripts and agents 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 and `closed_at` on deals keep the original timestamps, so timelines and reports read the way they did before.

```bash theme={null}
curl -X POST https://orbitforms.ai/api/v1/deals/import \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_source": "hubspot",
    "rows": [
      {
        "external_id": "9021",
        "name": "Acme renewal",
        "pipeline_name": "Sales",
        "stage_name": "Closed won",
        "amount": 12000,
        "close_date": "2026-03-14",
        "closed_at": "2026-03-14T16:20:00Z",
        "created_at": "2026-01-08T09:00:00Z",
        "owner_email": "ann@acme.com",
        "company_external_id": "77",
        "contact_external_ids": ["301", "302"]
      }
    ]
  }'
```

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

| What | Limit |
| - | - |
| Rows per call | 1,000 (contacts, companies, deals, unsubscribes), 500 (activities) |
| Import budget | 100,000 rows per workspace per hour, across every import endpoint. Rows count, calls do not, so batch as large as you like |
| Write requests | 300 per minute per credential (API key or OAuth token) |
| Single-record creates | 100 per hour per user for `POST /contacts`: use the imports for anything bigger |

A `429` carries `Retry-After` in seconds. An approved OAuth app has higher request limits (see [rate limits](https://docs.orbitforms.ai/developers/api/rate-limits.md)).

## 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. [Migrate from HubSpot with an agent](https://docs.orbitforms.ai/mcp/migrate-from-hubspot.md) maps every HubSpot object and engagement field to its Orbit counterpart.


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