> ## 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.

# CRM API

> Configure and run the OrbitForms CRM from code or an AI agent: schema, settings, custom fields, pipelines, companies, deals, tasks, notes and saved views.

Everything you can configure in **Settings → CRM** and every record on the Companies, Deals and Tasks pages is available here. The same endpoints power the [MCP server](https://docs.orbitforms.ai/mcp/tools.md), so an agent can set up a CRM end to end: learn the model, add fields, build pipelines, then create and move deals.

> [!NOTE]
>
> **Start with the schema.** `GET /api/v1/crm/schema` returns every object's standard and custom fields, the pipelines and their stages, all settings (with a description of each), tags, and team members (the IDs you use for `owner_user_id` and `assignee_user_id`).

## Scopes

| Scope | Allows |
| - | - |
| `crm:read` / `crm:write` | Read / change settings, custom fields, pipelines and stages, and saved contact views |
| `companies:read` / `companies:write` / `companies:delete` | Companies |
| `deals:read` / `deals:write` / `deals:delete` | Deals and the contacts linked to them (`deals:read` can also list pipelines) |
| `tasks:read` / `tasks:write` | Tasks (write includes completing and deleting) |
| `notes:read` / `notes:write` | Notes on contacts, companies and deals |
| `contacts:read` / `contacts:write` | Contacts, tags, and linking a contact to a company or owner |

Create a key with the **CRM agent** preset in the [Developer Portal](https://orbitforms.ai/developer-portal) to get all of these except deletes. The `:delete` scopes can be granted to API keys and to OAuth apps registered in the Developer Portal; assistants that connect over [MCP](https://docs.orbitforms.ai/mcp/overview.md) never hold them.

## Configuration

| Method | Endpoint | Description |
| - | - | - |
| `GET` | `/api/v1/crm/schema` | The whole CRM model in one call |
| `GET` | `/api/v1/crm/settings` | Settings for contacts, companies, deals and tasks |
| `GET` | `/api/v1/crm/settings/:object` | Settings for one object (`contact`, `company`, `deal`, `task`) |
| `PATCH` | `/api/v1/crm/settings/:object` | Change settings; keys you leave out keep their values, unknown keys are rejected |
| `GET` | `/api/v1/crm/fields?object=` | Custom fields for `contact`, `company` or `deal` |
| `POST` | `/api/v1/crm/fields` | Add a custom field |
| `PATCH` | `/api/v1/crm/fields/:object/:field_name` | Change a field's label, options or description |
| `DELETE` | `/api/v1/crm/fields/:object/:field_name` | Remove a field (stored values are kept) |
| `GET` | `/api/v1/pipelines` | Pipelines with ordered stages |
| `POST` | `/api/v1/pipelines` | Create a pipeline (optionally with stages) |
| `GET` / `PATCH` / `DELETE` | `/api/v1/pipelines/:id` | Read, rename / make default, delete (no deals allowed) |
| `POST` | `/api/v1/pipelines/:id/stages` | Add a stage |
| `PUT` | `/api/v1/pipelines/:id/stages` | Reorder stages: `{ "stage_ids": [...] }` |
| `PATCH` / `DELETE` | `/api/v1/pipelines/:id/stages/:stageId` | Change or delete a stage (`?move_deals_to=` when it has deals). Changing a stage's `kind` or passing `move_deals_to` also moves or closes every deal in it, so those two calls need `deals:write` as well as `crm:write` (`403 insufficient_scope` otherwise) |
| `GET` / `POST` | `/api/v1/contact-views` | Saved views of the Contacts table |
| `PATCH` / `DELETE` | `/api/v1/contact-views/:id` | Change or delete a view |

### Settings

| Object | Setting | Type | What it does |
| - | - | - | - |
| `contact` | `auto_link_company_by_domain` | boolean | Link new contacts to the company whose domain matches their email |
| `company` | `auto_create_from_contacts` | boolean | Create a company when a new contact names one that doesn't exist |
| `deal` | `default_currency` | string | ISO 4217 code for new deals |
| `deal` | `lost_reasons` | string\[] | Reasons offered when a deal is marked lost |
| `deal` | `require_lost_reason` | boolean | Require `lost_reason` before a deal moves to a lost stage |
| `deal` | `stale_after_days` | number \| null | Flag open deals with no stage change in this many days |
| `task` | `default_due_days` | number \| null | Due date for new tasks, in days |
| `task` | `default_assignee` | `creator` \| `record_owner` \| `none` | Who gets a task when no one is picked |

### Custom fields

Types are `string`, `number`, `boolean`, `date` and `url`; companies and deals also support `select` with `options`. `field_name` defaults to a snake\_case version of the label and, like the type, can't change later. Records store values in `custom_fields`, keyed by `field_name`, and a value for a defined field is checked against its type. A key that no field defines is handled differently per endpoint:

| Endpoint | A key with no field definition |
| - | - |
| Companies and deals (`POST`, `PATCH`) | Rejected with `400 invalid_request` |
| Contacts (`POST /api/v1/contacts`, `PATCH /api/v1/contacts/:id`) | Stored as sent, for keys from forms and integrations. Keys are up to 63 letters, digits, spaces, `_`, `.` or `-`; the value must be text (up to 2,000 characters), a number, `true`/`false`, or `null` to remove the key. A contact holds at most 300 keys and 64 KB of custom fields. Such keys don't appear in the schema until you define them |
| Contact import (`POST /api/v1/contacts/import`) | The request is rejected unless `create_missing_fields` is `true`, which creates each as a text field (that needs `crm:write`) |
| Contact merge (`values.custom_fields`) | Rejected with `400 invalid_request` |

```bash theme={null}
curl -X POST https://orbitforms.ai/api/v1/crm/fields \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "object": "deal", "label": "Lead source", "field_type": "select", "options": ["Inbound", "Outbound", "Partner"] }'
```

## Records

| Method | Endpoint | Description |
| - | - | - |
| `GET` / `POST` | `/api/v1/companies` | List (`search`, `domain`, `owner_user_id`) / create |
| `GET` / `PATCH` / `DELETE` | `/api/v1/companies/:id` | Read (with contact and open-deal counts), update, delete |
| `GET` / `POST` | `/api/v1/deals` | List (`pipeline_id`, `stage_id`, `status`, `owner_user_id`, `company_id`, `contact_id`, `search`) / create |
| `GET` / `PATCH` / `DELETE` | `/api/v1/deals/:id` | Read, update or move (`stage_id`), delete |
| `POST` | `/api/v1/deals/:id/contacts` | Link a contact: `{ "contact_id", "is_primary" }` |
| `DELETE` | `/api/v1/deals/:id/contacts/:contactId` | Unlink a contact |
| `GET` / `POST` | `/api/v1/tasks` | List (`status`, `assignee_user_id`, `contact_id`, `company_id`, `deal_id`, `due_before`, `due_after`) / create |
| `GET` / `PATCH` / `DELETE` | `/api/v1/tasks/:id` | Read, update (`"status": "done"` completes), delete |
| `GET` / `POST` | `/api/v1/notes` | List notes on one record / add a note |
| `PATCH` / `DELETE` | `/api/v1/notes/:id` | Edit or pin, delete |
| `GET` | `/api/v1/deals/:id/history` | Stage changes, newest first, with who moved it |
| `GET` | `/api/v1/deals/summary?pipeline_id=` | Per-stage counts, amounts and weighted amounts, status totals, stale count |
| `POST` | `/api/v1/contacts/import` | Import 1–1000 rows (`duplicate_mode`, `tag`, `workflow_id`, `workflow_scope`, `link_companies`, `create_missing_fields`). A workflow runs only when `workflow_id` is given, and that also needs `workflows:write`; `create_missing_fields` needs `crm:write` when it creates fields (`403 insufficient_scope` otherwise). `link_companies` defaults to the team's `company.auto_create_from_contacts` setting. Phones are stored in E.164 using the team's default country (Settings → Dialer) when they parse. A failed call that already saved rows returns `import_id` with the error |
| `GET` | `/api/v1/contact-imports` | Import history with counts and status |
| `GET` | `/api/v1/contact-imports/:id` | One import with its row errors |
| `POST` | `/api/v1/contact-imports/:id/undo` | Delete the contacts an import created and restore the ones it updated (`contacts:delete`, admins only). Large imports are undone in chunks: a `202` with `done: false` and `remaining` means call again |
| `POST` | `/api/v1/contacts/:id/merge` | Merge `secondary_contact_id` into `:id`: tags, notes, tasks, deal links, meetings, bookings, sequence enrollments, sent emails and calls move to `:id`, and the secondary is archived. Where both are enrolled in the same sequence, the secondary's enrollment is stopped. An archived contact can't be merged into (`409`) |

## Teams and pipeline access

Teams (member groups) and pipeline visibility use the `crm:read` / `crm:write` scopes; changes need a credential whose owner is a workspace admin.

| Method | Endpoint | Description |
| - | - | - |
| `GET` / `POST` | `/api/v1/member-groups` | List teams with their members / create `{ name, user_ids? }` |
| `GET` / `PATCH` / `DELETE` | `/api/v1/member-groups/:id` | Read, rename, delete (members stay in the workspace) |
| `PUT` / `POST` | `/api/v1/member-groups/:id/members` | Replace the member list `{ user_ids }` / add one `{ user_id }` |
| `GET` / `PUT` | `/api/v1/pipelines/:id/access` | `{ visibility: "everyone" \| "restricted", user_ids, group_ids }`; a restricted pipeline, its deals, notes and tasks are visible only to the people and teams listed (and admins) |

### Moving deals

`PATCH /api/v1/deals/:id` with a new `stage_id` moves the deal. Its `status` follows the stage type (`open`, `won`, `lost`), the move is recorded in stage history, and any workflow with the **Deal Stage Changed** trigger runs. When the team requires a lost reason, include `lost_reason` when moving to a lost stage.

```bash theme={null}
curl -X PATCH https://orbitforms.ai/api/v1/deals/DEAL_ID \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "stage_id": "STAGE_ID", "amount": 24000 }'
```

Creating a deal without `pipeline_id` uses the default pipeline and its first stage; `currency` defaults to the team's deal setting.

## Responses

Lists return `{ "data": [...], "meta": { "total", "page", "per_page", "total_pages" } }`; single records return `{ "data": {...} }`. Errors return `{ "error", "error_description" }` with a 4xx status, for example `409 in_use` when deleting a stage that still has deals without `move_deals_to`. A team can have at most 50 pipelines, 200 saved contact views and 100 teams (member groups); creating one more answers `409 limit_reached`.

> **[MCP tools](https://docs.orbitforms.ai/mcp/tools.md)**
>
> The same operations as MCP tools

> **[Rate Limits](https://docs.orbitforms.ai/developers/api/rate-limits.md)**
>
> API rate limit details


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