Company Hierarchy
This guide walks through the modeling of a B2B account with a tree of company records: a parent, its subsidiaries, and the brands or regions beneath them.
Each company is a node. Each parent-child link is an edge. The walkthrough below covers creating those companies, linking them, reading the tree back, and the limits and errors to plan for.
The worked example is a parent company, General Mills, with a child brand, Cheerios. The same calls discussed here will scale to deeper trees.
General Mills
└── Cheerios
What you are building
Company Hierarchy stores the account structure as data. A subsidiary stays its own company, with its own conversations and attributes, and an edge records which company is its parent. From any company you can read its direct parent, the companies directly beneath it, and the path from the root down to that company.
Linking uses the same company permissions as other company updates. There is no separate hierarchy role. These permissions are needed for all actions described in this document:
org.user.company.writeorg.user.company.read
Nodes, edges, and trees
The API revolves around these concepts:
Node. A company. Create it with Create company. Hierarchy endpoints link companies that already exist. A company with no parent edge is a root.
Edge. One parent-child link. In request and response bodies, sourceId is the parent company and targetId is the child company. The edge has its own id and type of kedge. That id is different from either company id. relType comes back as tree. displayName is filled in when you read a node.
Tree name. Each edge belongs to a named tree. Omit name and the API uses company_hierarchy. Send the same name on every later read and write for that tree. A company has at most one parent inside a given tree. A parent in one tree is independent of a parent in another tree.
company is the supported node type. sourceType, targetType, nodeType, and parentType all default to company. You can omit them. Any other value returns 400 with code badparam.
Step 1 | Add a company
Create company adds the company record. Until you add an edge, reading that company in the hierarchy shows a root whose only breadcrumb is itself.
name is the only required field. externalId is the practical handle for a sync. Use it with Get companies for matching purposes. Get companies accepts either externalId or filter (a name match).
curl --request POST \
--url https://orgname.api.kustomerapp.com/v1/companies \
--header 'Authorization: Bearer API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "General Mills",
"externalId": "erp-gm-001"
}'The data.id in the response is the company id. Store it against your external id. Create the child the same way:
curl --request POST \
--url https://orgname.api.kustomerapp.com/v1/companies \
--header 'Authorization: Bearer API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Cheerios",
"externalId": "erp-cheerios-001"
}'Sending both query parameters causes the request to ignore both. Company names must be at least 3 characters. See Create company for the full attribute list, including emails, domains, locations, and custom attributes.
To load many companies, use Bulk create companies. After the bulk job finishes, link each pair with the edge calls below, using the company ids from the bulk result.
Add an edge
Two endpoints write the same kind of edge. Both set the child company's parent in the named tree. If that child already has a parent in the tree, the new call replaces it. A company cannot be its own parent, and it also cannot set any of its own descendants.
Use Create a company hierarchy edge when you have both ids in hand. sourceId is the parent. targetId is the child.
curl --request POST \
--url https://orgname.api.kustomerapp.com/v1/kedge \
--header 'Authorization: Bearer API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"sourceId": "PARENT_COMPANY_ID",
"targetId": "CHILD_COMPANY_ID",
"name": "company_hierarchy"
}'Use Set a company's parent when you are already holding the child id. The path id is the child company. parentId is the new parent.
curl --request POST \
--url https://orgname.api.kustomerapp.com/v1/kedge/CHILD_COMPANY_ID/parent \
--header 'Authorization: Bearer API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"parentId": "PARENT_COMPANY_ID",
"name": "company_hierarchy"
}'name is optional in both bodies. Omitting it selects company_hierarchy.
A successful create returns 201 and a kedge resource:
{
"data": {
"type": "kedge",
"id": "64f1a2b3c4d5e6f7a8b9c0d1",
"attributes": {
"name": "company_hierarchy",
"relType": "tree",
"sourceId": "PARENT_COMPANY_ID",
"sourceType": "company",
"targetId": "CHILD_COMPANY_ID",
"targetType": "company",
"createdAt": "2026-08-14T22:01:34.620Z",
"updatedAt": "2026-08-14T22:01:34.620Z"
}
}
}Keep data.id. That is the edge id. Delete a company hierarchy edge expects this id while Get a company hierarchy node expects a company id.
Create and set-parent responses do not include displayName. Company names show up on the node read described next.
Each edge is one request. Build the tree from the root downward: link the parent to its children, then link those children to theirs. That order keeps each write within the depth limit.
View the tree
Get a company hierarchy node reads one company's position. The path id is a company id. name selects the tree and defaults to company_hierarchy. page and pageSize page that company's direct children. pageSize defaults to 100.
curl --request GET \
--url 'https://orgname.api.kustomerapp.com/v1/kedge/PARENT_COMPANY_ID?name=company_hierarchy&page=1&pageSize=100' \
--header 'Authorization: Bearer API_KEY'A 200 response wraps a kedge-node in data and pages the children in meta:
| Field | Meaning |
|---|---|
attributes.parent | The direct parent edge, or null when this company is a root. |
attributes.children | Direct child edges for this page. On this read, each child edge includes that child company's displayName. |
attributes.breadcrumbs | The path from the root down to this company, including this company. Each entry has id, type, and displayName. |
attributes.nodeType | company for a company node. |
attributes.displayName | This company's name. |
meta | page, pageSize, total, totalPages, and nextPage. nextPage is null on the last page. |
parent on this read is also an edge. Its displayName is the parent company's name. Child edges point the other way: displayName is the child. That resolved name is specific to the node read. Get a company's direct parent edge returns the edge without displayName.
{
"data": {
"type": "kedge-node",
"id": "PARENT_COMPANY_ID",
"attributes": {
"nodeType": "company",
"displayName": "General Mills",
"parent": null,
"children": [
{
"type": "kedge",
"id": "64f1a2b3c4d5e6f7a8b9c0d1",
"attributes": {
"name": "company_hierarchy",
"relType": "tree",
"sourceId": "PARENT_COMPANY_ID",
"sourceType": "company",
"targetId": "CHILD_COMPANY_ID",
"targetType": "company",
"displayName": "Cheerios",
"createdAt": "2026-08-14T22:01:34.620Z",
"updatedAt": "2026-08-14T22:01:34.620Z"
}
}
],
"breadcrumbs": [
{
"id": "PARENT_COMPANY_ID",
"type": "company",
"displayName": "General Mills"
}
]
}
},
"meta": {
"page": 1,
"pageSize": 100,
"total": 1,
"totalPages": 1,
"nextPage": null
}
}Children in this payload are the next level only. To walk further, page until nextPage is null, then call the same endpoint for each child targetId. To find the root from a company deep in the tree, read that company and follow breadcrumbs: the first entry is the root, and the last entry is the company you asked for.
Get a company's direct parent edge returns only the parent edge. A company that is currently a root responds with 404. The node read is the call that shows a root explicitly, as parent: null and a one-entry breadcrumb list.
curl --request GET \
--url 'https://orgname.api.kustomerapp.com/v1/kedge/CHILD_COMPANY_ID/parent?name=company_hierarchy' \
--header 'Authorization: Bearer API_KEY'Read the root, page its children, and repeat down the levels you need. Each response contains one company and its direct children.
Add more companies to the tree
A new subsidiary, region, or brand is another company node. The edge you add afterward is the link, and it is a separate request.
- Create the company with Create company, or look up an existing one with Get companies or Get company by ID.
- Link it with
POST /kedgeorPOST /kedge/{id}/parent, using the existing company assourceId/parentIdand the new company as the child.
The new company can already have children of its own. Those child edges stay pointed at it. Replacing its parent moves that whole subtree to the new location, and the cycle, depth, and size checks below still apply.
Leaving a company unlinked is valid. It remains a normal company, and a hierarchy read shows it as a root.
Node types other than company are rejected. To represent something that is not a company, create it with its own API (for example a customer, or a custom object) and keep the hierarchy edge between companies.
Move, unlink, and delete
Change a parent. Send POST /kedge or POST /kedge/{id}/parent again with the new parent id and the same tree name. The child's previous parent edge in that tree is replaced. Edges that list this company as sourceId stay as they are, so its children remain under it.
Unlink by company id. Remove a company's parent deletes the parent edge. The company record remains. Its children remain attached, so the company becomes a root.
curl --request DELETE \
--url 'https://orgname.api.kustomerapp.com/v1/kedge/CHILD_COMPANY_ID/parent?name=company_hierarchy' \
--header 'Authorization: Bearer API_KEY'A success response is 204 with no body.
Unlink by edge id. Delete a company hierarchy edge deletes one edge. The path id is the kedge id from the create response or from a node read, not a company id. Success is 204. A missing edge is 404. If another request changes that same edge at the same time, the response is 409 with code conflict; retry the delete.
Delete a company. Update company attributes with deleted set to true deletes the company record. If that company is still a parent in any hierarchy tree, the API returns 409 with code conflict, names the first blocking child and the tree, and leaves the company in place. Unlink that child with DELETE /kedge/{childId}/parent?name=<tree>, using the tree name from the error, or delete the child first. The check covers every tree the company participates in, including trees other than company_hierarchy. Clear each tree that still names a blocking child, then retry the delete.
Clear child links from the leaves upward. Children stay in place until you unlink or delete each one.
Limits
The tree of any single root is at most 20 levels from the root down to a leaf, and at most 2000 companies. The 2,000-company limit applies to the subtree beneath each root, even when multiple roots use the same tree name. An edge write that would move a subtree under a root and push that root’s total past 2,000 companies is rejected.
You can create multiple roots. Each root's tree has its own 20-level and 2,000-company limits, so separate trees can contain more than 2,000 companies in total.
| HTTP status | code | When it happens |
|---|---|---|
| 400 | badparam | sourceId, targetId, or parentId does not reference an existing, readable node. |
| 400 | badparam | The parent id and the child id are the same company. |
| 400 | badparam | The edge would create a cycle, such as placing a company under one of its own descendants. |
| 400 | badparam | The edge would put a node past 20 levels from the root. |
| 400 | badparam | The edge would grow the destination tree past 2000 companies. |
| 400 | badparam | The parent company has already been deleted. |
| 400 | badparam | sourceType, targetType, nodeType, or parentType is not a supported node type. |
| 409 | conflict | Another write to this hierarchy tree is in progress. Retry the request. |
| 409 | conflict | On edge delete, another request modified that same edge. Retry the request. |
| 404 | not_found | GET /kedge/{id}/parent for a company that is currently a root, or a delete-by-edge-id for an edge that does not exist. |
Wide levels are paged, not truncated. Keep requesting page until nextPage is null. total is the count of direct children, which is the value to compare against your source system.
A sync that stays within the model
- Create or match every company. Persist Kustomer company ids next to your external ids. Use
externalIdon the company so a later Get companies call can find them. - Link from the root downward, one edge per parent-child pair, always with the same
name(or always omit it, which selectscompany_hierarchy). - Read the root with
GET /kedge/{id}and pagechildrenuntiltotalmatches the number of direct subsidiaries you sent. - On a later change, send the edge create again to move a company under a new parent. Send
DELETE /kedge/{id}/parentwhen the external system removes the relationship. - On
409withcodeconflict, retry that single write. The tree is locked by another in-progress write, or the edge you tried to delete changed concurrently.
Pass name explicitly in scripts even though the default is company_hierarchy. A delete that is blocked names the tree in the error; reuse that exact string when you unlink the blocking child.
Troubleshooting Calls
Check that requests were sent to your org host. Getting started with Kustomer API describes why the org subdomain is required.
https://orgname.api.kustomerapp.com/v1
Authorization: Bearer API_KEY
Content-Type: application/jsonUpdated about 1 hour ago