# Model company hierarchies

> Represent B2B accounts as trees of companies with the hierarchy API. Link parents and subsidiaries, read a company's parent, children and breadcrumbs, move and unlink branches, delete companies safely, and sync a tree from your CRM within the depth and size limits.

Source: https://developer.kustomer.com/docs/guides/company-hierarchy

B2B accounts are rarely flat. A parent company owns subsidiaries, which own brands or regional offices, and your agents need to see where a customer's company sits in that tree. The hierarchy API stores this structure as edges between existing companies.

```mermaid
flowchart TD
  GM["General Mills<br/>(root)"] --> C["Cheerios"]
  GM --> N["Nature Valley"]
  N --> NE["Nature Valley Europe"]
  N --> NA["Nature Valley North America"]
```

Each box is a company with its own conversations, attributes and custom objects. Each arrow is an **edge** recording which company is the parent.

## How hierarchies work

- **Nodes are companies.** Create them with the [companies API](https://developer.kustomer.com/api/core-resources/create-company) first. The hierarchy endpoints only link companies that already exist. A company with no parent is a **root**.
- **An edge is one parent-child link.** It's a resource of type `kedge` with its own ID, separate from either company's ID. `sourceId` is the parent and `targetId` is the child.
- **Edges belong to a named tree.** `name` defaults to `company_hierarchy`. A company has at most one parent per tree, and trees are independent, so the same company can have a different parent in a `billing` tree than in a `sales` tree.
- **Only companies are supported.** `sourceType`, `targetType`, `nodeType` and `parentType` default to `company`; any other value returns `400 badparam`.
- **No extra roles.** Reads need `org.user.company.read` (or the permission-set equivalent `org.permission.company.read`), and writes need `org.user.company.write` (`org.permission.company.update`).

> **Always send the tree name:** The default works, but pass `name` explicitly in scripts. Error messages for blocked deletes name the tree, and you'll reuse that exact string to fix them.

## Link companies

Create the parent and child as usual, storing each Kustomer ID against your system's ID in `externalId`:

```bash
curl -X POST "https://acme.api.kustomerapp.com/v1/companies" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "General Mills", "externalId": "erp-gm-001" }'
```

Then add an edge. There are two equivalent endpoints; use whichever matches the IDs you're holding.

**Create an edge**

`POST /v1/kedge` ([reference](https://developer.kustomer.com/api/core-resources/create-a-company-hierarchy-edge))

```bash
curl -X POST "https://acme.api.kustomerapp.com/v1/kedge" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "sourceId": "PARENT_ID", "targetId": "CHILD_ID", "name": "company_hierarchy" }'
```

**Set a company's parent**

`POST /v1/kedge/{id}/parent` ([reference](https://developer.kustomer.com/api/core-resources/set-a-companys-parent))

```bash
curl -X POST "https://acme.api.kustomerapp.com/v1/kedge/CHILD_ID/parent" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "parentId": "PARENT_ID", "name": "company_hierarchy" }'
```

Both return `201` with the edge:

```json
{
  "data": {
    "type": "kedge",
    "id": "64f1a2b3c4d5e6f7a8b9c0d1",
    "attributes": {
      "name": "company_hierarchy",
      "relType": "tree",
      "sourceId": "PARENT_ID",
      "sourceType": "company",
      "targetId": "CHILD_ID",
      "targetType": "company",
      "createdAt": "2026-08-14T22:01:34.620Z",
      "updatedAt": "2026-08-14T22:01:34.620Z"
    }
  }
}
```

If the child already has a parent in that tree, the new edge **replaces** it. Keep `data.id` if you plan to delete edges by ID later.

Each edge is one request. Link from the root downward, so every write lands in a tree that's already within the [limits](#limits).

## Read the tree

`GET /v1/kedge/{id}` ([reference](https://developer.kustomer.com/api/core-resources/get-a-company-hierarchy-node))

Pass a **company** ID. The response is a `kedge-node` with the company's parent, one page of its direct children, and the path from the root:

```bash
curl "https://acme.api.kustomerapp.com/v1/kedge/PARENT_ID?name=company_hierarchy&page=1&pageSize=100" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY"
```

Children are one level deep. To walk the whole tree, page through `children` until `nextPage` is `null`, then read each child's `targetId` the same way. To find the root of any company, read it and take `breadcrumbs[0]`.

[`GET /v1/kedge/{id}/parent`](https://developer.kustomer.com/api/core-resources/get-a-companys-direct-parent-edge) returns just the parent edge, without names, and responds `404` for a root.

```ts title="walk-tree.ts"
const BASE = `https://${process.env.KUSTOMER_ORG}.api.kustomerapp.com/v1`
const headers = { Authorization: `Bearer ${process.env.KUSTOMER_API_KEY}` }

interface Edge { id: string; attributes: { targetId: string; displayName?: string } }

/** Prints the subtree under `companyId`, one company per line, indented by depth. */
async function walk(companyId: string, tree = 'company_hierarchy', depth = 0): Promise<void> {
  for (let page = 1; page; ) {
    const res = await fetch(`${BASE}/kedge/${companyId}?name=${tree}&page=${page}&pageSize=100`, { headers })
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
    const { data, meta } = await res.json()
    if (page === 1) console.log(`${'  '.repeat(depth)}${data.attributes.displayName}`)
    for (const child of data.attributes.children as Edge[]) await walk(child.attributes.targetId, tree, depth + 1)
    page = meta.nextPage
  }
}

await walk(process.argv[2])
```

## Move and unlink

| To                             | Call                                                                                                                                                                                                                                         | Result                                                                    |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Move a company to a new parent | [`POST /v1/kedge`](https://developer.kustomer.com/api/core-resources/create-a-company-hierarchy-edge) or [`POST /v1/kedge/{id}/parent`](https://developer.kustomer.com/api/core-resources/set-a-companys-parent) again, with the same `name` | The old parent edge is replaced. The company's own children move with it. |
| Make a company a root          | [`DELETE /v1/kedge/{id}/parent?name=…`](https://developer.kustomer.com/api/core-resources/remove-a-companys-parent) with the **company** ID                                                                                                  | `204`. The company and its children stay; it becomes a root.              |
| Delete one edge                | [`DELETE /v1/kedge/{id}`](https://developer.kustomer.com/api/core-resources/delete-a-company-hierarchy-edge) with the **edge** ID                                                                                                            | `204`, or `404` if it doesn't exist.                                      |

A company can't be its own parent, and can't be placed under one of its own descendants.

## Delete companies

Companies are deleted by setting `deleted: true`, not with a `DELETE` request:

```bash title="One company"
curl -X PUT "https://acme.api.kustomerapp.com/v1/companies/COMPANY_ID" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "deleted": true }'
```

```bash title="Up to 100, each listed"
curl -X PUT "https://acme.api.kustomerapp.com/v1/companies/bulk" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[{ "id": "COMPANY_ID_1", "deleted": true }, { "id": "COMPANY_ID_2", "deleted": true }]'
```

```bash title="Up to 100, same change"
curl -X PUT "https://acme.api.kustomerapp.com/v1/companies/bulk?ids=COMPANY_ID_1,COMPANY_ID_2" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "deleted": true }'
```

Bulk updates run asynchronously; see [bulk operations](https://developer.kustomer.com/docs/guides/bulk-operations) for polling the result.

> **Parents can't be deleted:** Deleting a company that still has children in **any** tree returns `409 conflict`, naming the first blocking child and its tree, and leaves the company in place. Unlink that child with `DELETE /v1/kedge/{childId}/parent?name=<tree>` using the tree name from the error, or delete the child first, then retry. To remove a whole branch, work from the leaves up.

## Limits

- A tree can be at most **20 levels** deep, from root to leaf.
- A tree can hold at most **2,000 companies**. This is checked when an edge moves a subtree into another tree.
- You can have any number of roots. Each root's tree has its own limits.

Wide levels are paged, never truncated.

| Status | `code`      | Cause                                                                                                                     |
| ------ | ----------- | ------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `badparam`  | A `sourceId`, `targetId` or `parentId` doesn't reference an existing company you can read, or the parent has been deleted |
| `400`  | `badparam`  | The parent and child are the same company, or the edge would create a cycle                                               |
| `400`  | `badparam`  | The edge would exceed 20 levels or 2,000 companies                                                                        |
| `400`  | `badparam`  | An unsupported `sourceType`, `targetType`, `nodeType` or `parentType`                                                     |
| `404`  | `not_found` | Reading the parent of a root, or deleting an edge ID that doesn't exist                                                   |
| `409`  | `conflict`  | Another write to the same tree is in progress, or the edge changed during a delete. Retry the request.                    |
| `409`  | `conflict`  | Deleting a company that's still a parent (see above)                                                                      |

## Sync a hierarchy from your CRM

#### Create or match every company

Upsert companies by `externalId` (see [Sync customers from a CRM](https://developer.kustomer.com/docs/guides/sync-customers-from-crm#3-companies)) and store each Kustomer company ID next to your record. For large loads, use [bulk create](https://developer.kustomer.com/api/core-resources/bulk-create-companies) and take IDs from the bulk result.

#### Link from the root down

One edge per parent-child pair, always with the same `name`. Process parents before children so no write exceeds the depth limit mid-sync.

#### Verify

Read each parent with `GET /v1/kedge/{id}` and compare `meta.total` with the number of direct children in your CRM.

#### Apply changes incrementally

When a company moves, send the edge again with its new parent. When the CRM drops a relationship, call `DELETE /v1/kedge/{id}/parent`. Retry single writes that return `409 conflict`.

> **For AI agents:** Edge IDs and company IDs are both 24-character hex strings. `GET /v1/kedge/{id}` and `DELETE /v1/kedge/{id}/parent` take a **company** ID; only `DELETE /v1/kedge/{id}` takes an **edge** ID.
