curl --request POST \
--url https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert \
--header 'Content-Type: application/json' \
--header 'x-client-key: <api-key>' \
--header 'x-client-secret: <api-key>' \
--data '
{
"hierarchies": [
{
"externalId": "OPS",
"name": "Operations",
"description": "Company reporting structure",
"color": "#7C5CFC",
"levels": [
{
"title": "Region",
"order": 1,
"isSite": false
},
{
"title": "District",
"order": 2,
"isSite": false
},
{
"title": "Store",
"order": 3,
"isSite": true
}
]
}
]
}
'import requests
url = "https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert"
payload = { "hierarchies": [
{
"externalId": "OPS",
"name": "Operations",
"description": "Company reporting structure",
"color": "#7C5CFC",
"levels": [
{
"title": "Region",
"order": 1,
"isSite": False
},
{
"title": "District",
"order": 2,
"isSite": False
},
{
"title": "Store",
"order": 3,
"isSite": True
}
]
}
] }
headers = {
"x-client-key": "<api-key>",
"x-client-secret": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'x-client-key': '<api-key>',
'x-client-secret': '<api-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
hierarchies: [
{
externalId: 'OPS',
name: 'Operations',
description: 'Company reporting structure',
color: '#7C5CFC',
levels: [
{title: 'Region', order: 1, isSite: false},
{title: 'District', order: 2, isSite: false},
{title: 'Store', order: 3, isSite: true}
]
}
]
})
};
fetch('https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'hierarchies' => [
[
'externalId' => 'OPS',
'name' => 'Operations',
'description' => 'Company reporting structure',
'color' => '#7C5CFC',
'levels' => [
[
'title' => 'Region',
'order' => 1,
'isSite' => false
],
[
'title' => 'District',
'order' => 2,
'isSite' => false
],
[
'title' => 'Store',
'order' => 3,
'isSite' => true
]
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-client-key: <api-key>",
"x-client-secret: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert"
payload := strings.NewReader("{\n \"hierarchies\": [\n {\n \"externalId\": \"OPS\",\n \"name\": \"Operations\",\n \"description\": \"Company reporting structure\",\n \"color\": \"#7C5CFC\",\n \"levels\": [\n {\n \"title\": \"Region\",\n \"order\": 1,\n \"isSite\": false\n },\n {\n \"title\": \"District\",\n \"order\": 2,\n \"isSite\": false\n },\n {\n \"title\": \"Store\",\n \"order\": 3,\n \"isSite\": true\n }\n ]\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-client-key", "<api-key>")
req.Header.Add("x-client-secret", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert")
.header("x-client-key", "<api-key>")
.header("x-client-secret", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"hierarchies\": [\n {\n \"externalId\": \"OPS\",\n \"name\": \"Operations\",\n \"description\": \"Company reporting structure\",\n \"color\": \"#7C5CFC\",\n \"levels\": [\n {\n \"title\": \"Region\",\n \"order\": 1,\n \"isSite\": false\n },\n {\n \"title\": \"District\",\n \"order\": 2,\n \"isSite\": false\n },\n {\n \"title\": \"Store\",\n \"order\": 3,\n \"isSite\": true\n }\n ]\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-client-key"] = '<api-key>'
request["x-client-secret"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"hierarchies\": [\n {\n \"externalId\": \"OPS\",\n \"name\": \"Operations\",\n \"description\": \"Company reporting structure\",\n \"color\": \"#7C5CFC\",\n \"levels\": [\n {\n \"title\": \"Region\",\n \"order\": 1,\n \"isSite\": false\n },\n {\n \"title\": \"District\",\n \"order\": 2,\n \"isSite\": false\n },\n {\n \"title\": \"Store\",\n \"order\": 3,\n \"isSite\": true\n }\n ]\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"data": [
{
"externalId": "OPS",
"id": "9c2a1b7e-4d3f-4a11-9c88-2f0e5a6b1c34",
"name": "Operations",
"created": true,
"levelsCreated": 3
}
],
"extra_meta": {
"message": "Hierarchies upserted"
}
}Bulk Upsert Hierarchies
Creates or extends up to 100 hierarchies per call, each with 1–20 levels.
Identity resolution, in priority order: externalId → id → name. Unmatched rows are created; the first hierarchy in a workspace becomes its default.
Level rules (enforced on create): exactly one level must be marked isSite: true — the leaf level that real, task-bearing locations live on — and order values must be unique within the hierarchy. Orders are stored as you send them.
On a match, levels are appended by title: a level whose title already exists is left alone, anything new is appended after the hierarchy’s current last level (the server assigns its order, so the order you send is ignored), and a level marked isSite: true is skipped — a hierarchy keeps the single site level it was created with. Levels are never renamed, reordered, or deleted through this endpoint, so an existing hierarchy’s shape is never destructively rewritten.
No re-parenting. This endpoint shapes the hierarchy; it does not move locations within it. See Bulk Upsert Locations for what placement changes are and are not supported.
Rows are processed one at a time rather than as one atomic batch: if a later row fails, earlier rows stay created. Re-running the same payload is safe, because every row is matched before it is created.
Requires the Public API and Location Attributes workspace features plus CAN_MANAGE_LOCATIONS.
curl --request POST \
--url https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert \
--header 'Content-Type: application/json' \
--header 'x-client-key: <api-key>' \
--header 'x-client-secret: <api-key>' \
--data '
{
"hierarchies": [
{
"externalId": "OPS",
"name": "Operations",
"description": "Company reporting structure",
"color": "#7C5CFC",
"levels": [
{
"title": "Region",
"order": 1,
"isSite": false
},
{
"title": "District",
"order": 2,
"isSite": false
},
{
"title": "Store",
"order": 3,
"isSite": true
}
]
}
]
}
'import requests
url = "https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert"
payload = { "hierarchies": [
{
"externalId": "OPS",
"name": "Operations",
"description": "Company reporting structure",
"color": "#7C5CFC",
"levels": [
{
"title": "Region",
"order": 1,
"isSite": False
},
{
"title": "District",
"order": 2,
"isSite": False
},
{
"title": "Store",
"order": 3,
"isSite": True
}
]
}
] }
headers = {
"x-client-key": "<api-key>",
"x-client-secret": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'x-client-key': '<api-key>',
'x-client-secret': '<api-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
hierarchies: [
{
externalId: 'OPS',
name: 'Operations',
description: 'Company reporting structure',
color: '#7C5CFC',
levels: [
{title: 'Region', order: 1, isSite: false},
{title: 'District', order: 2, isSite: false},
{title: 'Store', order: 3, isSite: true}
]
}
]
})
};
fetch('https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'hierarchies' => [
[
'externalId' => 'OPS',
'name' => 'Operations',
'description' => 'Company reporting structure',
'color' => '#7C5CFC',
'levels' => [
[
'title' => 'Region',
'order' => 1,
'isSite' => false
],
[
'title' => 'District',
'order' => 2,
'isSite' => false
],
[
'title' => 'Store',
'order' => 3,
'isSite' => true
]
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-client-key: <api-key>",
"x-client-secret: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert"
payload := strings.NewReader("{\n \"hierarchies\": [\n {\n \"externalId\": \"OPS\",\n \"name\": \"Operations\",\n \"description\": \"Company reporting structure\",\n \"color\": \"#7C5CFC\",\n \"levels\": [\n {\n \"title\": \"Region\",\n \"order\": 1,\n \"isSite\": false\n },\n {\n \"title\": \"District\",\n \"order\": 2,\n \"isSite\": false\n },\n {\n \"title\": \"Store\",\n \"order\": 3,\n \"isSite\": true\n }\n ]\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-client-key", "<api-key>")
req.Header.Add("x-client-secret", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert")
.header("x-client-key", "<api-key>")
.header("x-client-secret", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"hierarchies\": [\n {\n \"externalId\": \"OPS\",\n \"name\": \"Operations\",\n \"description\": \"Company reporting structure\",\n \"color\": \"#7C5CFC\",\n \"levels\": [\n {\n \"title\": \"Region\",\n \"order\": 1,\n \"isSite\": false\n },\n {\n \"title\": \"District\",\n \"order\": 2,\n \"isSite\": false\n },\n {\n \"title\": \"Store\",\n \"order\": 3,\n \"isSite\": true\n }\n ]\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.xenia.team/api/v1/mgt/enterprise/workspaces/{workspaceId}/hierarchy/bulk-upsert")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-client-key"] = '<api-key>'
request["x-client-secret"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"hierarchies\": [\n {\n \"externalId\": \"OPS\",\n \"name\": \"Operations\",\n \"description\": \"Company reporting structure\",\n \"color\": \"#7C5CFC\",\n \"levels\": [\n {\n \"title\": \"Region\",\n \"order\": 1,\n \"isSite\": false\n },\n {\n \"title\": \"District\",\n \"order\": 2,\n \"isSite\": false\n },\n {\n \"title\": \"Store\",\n \"order\": 3,\n \"isSite\": true\n }\n ]\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"data": [
{
"externalId": "OPS",
"id": "9c2a1b7e-4d3f-4a11-9c88-2f0e5a6b1c34",
"name": "Operations",
"created": true,
"levelsCreated": 3
}
],
"extra_meta": {
"message": "Hierarchies upserted"
}
}Path Parameters
UUID of the workspace. Must be the workspace your API key is bound to.
Body
1 - 100 elementsShow child attributes
Show child attributes
Response
One row per input row. levelsCreated counts only the levels this call added.