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.
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
kedgewith its own ID, separate from either company's ID.sourceIdis the parent andtargetIdis the child. - Edges belong to a named tree.
namedefaults tocompany_hierarchy. A company has at most one parent per tree, and trees are independent, so the same company can have a different parent in abillingtree than in asalestree. - Only companies are supported.
sourceType,targetType,nodeTypeandparentTypedefault tocompany; any other value returns400 badparam. - No extra roles. Reads need
org.user.company.read(or the permission-set equivalentorg.permission.company.read), and writes needorg.user.company.write(org.permission.company.update).
Link companies
Create the parent and child as usual, storing each Kustomer ID against your system's ID in externalId:
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.
/v1/kedgecurl -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" }'/v1/kedge/{id}/parentcurl -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:
{
"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:
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- This company's name.
attributes.parentkedge | null- The edge to this company's parent, with the parent's name in
displayName.nullfor a root. attributes.childrenkedge[]- Edges to direct children on this page, each with the child's name in
displayName. attributes.breadcrumbsobject[]- Every company from the root down to this one, as
{ id, type, displayName }. The first entry is the root; the last is the company you asked for. metaobjectpage,pageSize(default 100),totaldirect children,totalPagesandnextPage, which isnullon the last page.
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.
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
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.
Sync a hierarchy from your CRM
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.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 comparemeta.totalwith 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 return409 conflict.