> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useorbitcrm.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]
>
> 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.

```bash theme={null}
curl -X POST https://app.useorbitcrm.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` (an app: 100 per workspace, 1,000 once approved): 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.useorbitcrm.ai/developers/api/rate-limits.md)).

## 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](https://docs.useorbitcrm.ai/developers/webhooks/overview.md#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](https://docs.useorbitcrm.ai/developers/migrating-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.