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

# Introduction to companies

> Group contacts under the company they belong to, and attach the account data your agents need during a conversation.

## What is a company?

A **company** is an account record that contacts belong to. If three people from Acme Retail open sessions, they can all point at one Acme Retail company instead of each carrying their own copy of the plan tier, the CSM's name, and the account id.

A contact can belong to several companies, and a company can have any number of contacts.

## Identity is your key, not ours

Every company is identified by `external_id` — whatever you already call it in your own system. A Zoho account id, a merchant id, a tenant slug. Writes resolve on that key, so the same `POST /companies` call creates the company the first time and updates it every time after.

`name` is for display only. It is not unique, so two merchants can both be called "Acme" and renaming one is never a collision.

## What you can do

* **Create or update a company** → [Upsert a company](/api-reference/companies/upsert)
* **Assign the teammate who owns it** → [Set the owner](/api-reference/companies/set-owner)
* **Add a company to a contact** → [Link a contact](/api-reference/contacts/companies/link)
* **Read a contact's companies** → [List a contact's companies](/api-reference/contacts/companies/list)
* **Remove one company from a contact** → [Unlink a contact](/api-reference/contacts/companies/unlink)

Linking is strict: the `external_id` must already exist. An unknown key returns a 400 rather than creating a company, so a typo in a sync script cannot quietly fill your account list with junk.

## Attributes

`custom_data` holds flat key/value attributes — strings, numbers, and booleans. Sending an attribute merges it into what is already there, and sending it as `null` clears that one key. Keys you do not mention are left alone.

```json theme={"dark"}
{
  "external_id": "acct_88213",
  "name": "Acme Retail",
  "custom_data": { "plan": "growth", "seats": 40, "csm": "sara@acme.test" }
}
```

<Note>
  Company attributes are not visible to the AI agent. They are available to your
  workflows through the **Get Contact Companies** action, and to your agents in
  the inbox, but they are never added to the model's prompt.
</Note>

## Who owns the account

A company can name one teammate as its owner, the CSM or account manager who looks after it. The owner shows on the company's page and on the companies card in the inbox, so an agent picking up a session can see who to ask.

Name the owner by OpenCX user id or by email, whichever you already hold:

```json theme={"dark"}
{ "owner": { "email": "sara@yourcompany.test" } }
```

Email matching ignores capitalization, so an address copied straight out of a CRM works. Send `"owner": null` to leave the company unowned, which is the state every company starts in.

You can set the owner while creating the company, in the same [upsert](/api-reference/companies/upsert) call:

```json theme={"dark"}
{
  "external_id": "acct_88213",
  "name": "Acme Retail",
  "owner": { "email": "sara@yourcompany.test" }
}
```

Omitting `owner` leaves ownership alone, so a nightly attribute sync never reassigns an account by accident.

The owner must be a member of your organization. An email that belongs to nobody on your team returns a 400 rather than quietly clearing the field. The error reads the same whether the address is unknown to OpenCX or belongs to another organization, so it tells you nothing about who exists outside your own team.

When a teammate leaves your organization, every company they owned becomes unowned.

## Syncing from a CRM

A CRM sync usually pushes many links at once. [Link many contacts](/api-reference/companies/bulk-link) takes up to 100 in a single call and isolates failures: a row naming a company that was never created returns its own error, and the other 99 links still land. Results come back in request order with a summary.

Name each contact whichever way you already hold it — `contact_id`, `contact_email`, or `contact_phone`, exactly one per row — so you don't have to resolve every contact to an OpenCX id first. Each result carries the resolved `contact_id`, which is worth storing against your own record to skip the lookup next time.

```json theme={"dark"}
{
  "links": [
    { "contact_email": "sara@acme.test", "external_id": "acct_88213" },
    { "contact_phone": "+15551234567", "external_id": "acct_88213" },
    { "contact_id": "8f3c...", "external_id": "acct_44100" }
  ]
}
```

Phone matching is forgiving about spelling: `+15551234567` and `15551234567` find the same contact. If a number matches more than one contact, that row reports an error instead of guessing — name that contact by `contact_id`.

## Scopes

Reads need `companies:read`; writes need `companies:write`.

These are separate from `contacts:*` on purpose. Company records usually carry data pulled from your CRM, so a key that manages contacts does not automatically get to read or rewrite the account graph.

## Companies in workflows

Five workflow actions cover the same ground without any API calls:

* **Create or Update Company**
* **Link Contact to Company**
* **Get Contact Companies** — returns `found`, `count`, and the list, so you can branch on a contact having no company
* **Set Company Owner** — takes an email or an agent id, so a workflow can assign the owner from a CRM lookup
* **Unlink Contact from Company** — removes one company and leaves the contact's others alone

Get Contact Companies returns each company's owner as `agentOwnerId`, `agentOwnerName`, and `agentOwnerEmail`. The id is the same numeric agent id **Change Ticket Assignee** takes, so a workflow can route a session to whoever owns the account. Check `agentOwnerId` is set before assigning, since most companies have no owner.

Linking a contact does not fire the **Contact Updated** trigger. Nothing on the contact record changes, and a workflow that links on Contact Updated would otherwise re-trigger itself.
