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.

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.

General Mills
(root)

Cheerios

Nature Valley

Nature Valley Europe

Nature Valley North America

Diagram source
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 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).

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

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

post/v1/kedge
Shell
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" }'

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.

Read the tree

get/v1/kedge/{id}

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:

Shell
curl "https://acme.api.kustomerapp.com/v1/kedge/PARENT_ID?name=company_hierarchy&page=1&pageSize=100" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY"
attributes.displayNamestring
attributes.parentkedge | null
attributes.childrenkedge[]
attributes.breadcrumbsobject[]
metaobject

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 returns just the parent edge, without names, and responds 404 for a root.

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])
ToCallResult
Move a company to a new parentPOST /v1/kedge or POST /v1/kedge/{id}/parent again, with the same nameThe old parent edge is replaced. The company's own children move with it.
Make a company a rootDELETE /v1/kedge/{id}/parent?name=… with the company ID204. The company and its children stay; it becomes a root.
Delete one edgeDELETE /v1/kedge/{id} with the edge ID204, 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:

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 }'

Bulk updates run asynchronously; see bulk operations for polling the result.

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.

StatuscodeCause
400badparamA sourceId, targetId or parentId doesn't reference an existing company you can read, or the parent has been deleted
400badparamThe parent and child are the same company, or the edge would create a cycle
400badparamThe edge would exceed 20 levels or 2,000 companies
400badparamAn unsupported sourceType, targetType, nodeType or parentType
404not_foundReading the parent of a root, or deleting an edge ID that doesn't exist
409conflictAnother write to the same tree is in progress, or the edge changed during a delete. Retry the request.
409conflictDeleting a company that's still a parent (see above)

Sync a hierarchy from your CRM

  1. Create or match every company

    Upsert companies by externalId (see Sync customers from a CRM) and store each Kustomer company ID next to your record. For large loads, use bulk create and take IDs from the bulk result.

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

  3. Verify

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

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