curl --request POST \
--url https://api.xenia.team/api/v1/mgt/users/provision \
--header 'Content-Type: application/json' \
--header 'x-client-key: <api-key>' \
--header 'x-client-secret: <api-key>' \
--data '
{
"users": [
{
"email": "jane.smith@example.com",
"role_id": "4ccf0b65-fb99-40da-92d6-3f98730df8b2",
"location_ids": [
"a13ddb59-3e5c-4bdd-8673-fc913b93b9db"
],
"first_name": "Jane",
"last_name": "Smith"
}
],
"location_mode": "add",
"notification": "none"
}
'import requests
url = "https://api.xenia.team/api/v1/mgt/users/provision"
payload = {
"users": [
{
"email": "jane.smith@example.com",
"role_id": "4ccf0b65-fb99-40da-92d6-3f98730df8b2",
"location_ids": ["a13ddb59-3e5c-4bdd-8673-fc913b93b9db"],
"first_name": "Jane",
"last_name": "Smith"
}
],
"location_mode": "add",
"notification": "none"
}
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({
users: [
{
email: 'jane.smith@example.com',
role_id: '4ccf0b65-fb99-40da-92d6-3f98730df8b2',
location_ids: ['a13ddb59-3e5c-4bdd-8673-fc913b93b9db'],
first_name: 'Jane',
last_name: 'Smith'
}
],
location_mode: 'add',
notification: 'none'
})
};
fetch('https://api.xenia.team/api/v1/mgt/users/provision', 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/users/provision",
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([
'users' => [
[
'email' => 'jane.smith@example.com',
'role_id' => '4ccf0b65-fb99-40da-92d6-3f98730df8b2',
'location_ids' => [
'a13ddb59-3e5c-4bdd-8673-fc913b93b9db'
],
'first_name' => 'Jane',
'last_name' => 'Smith'
]
],
'location_mode' => 'add',
'notification' => 'none'
]),
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/users/provision"
payload := strings.NewReader("{\n \"users\": [\n {\n \"email\": \"jane.smith@example.com\",\n \"role_id\": \"4ccf0b65-fb99-40da-92d6-3f98730df8b2\",\n \"location_ids\": [\n \"a13ddb59-3e5c-4bdd-8673-fc913b93b9db\"\n ],\n \"first_name\": \"Jane\",\n \"last_name\": \"Smith\"\n }\n ],\n \"location_mode\": \"add\",\n \"notification\": \"none\"\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/users/provision")
.header("x-client-key", "<api-key>")
.header("x-client-secret", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"users\": [\n {\n \"email\": \"jane.smith@example.com\",\n \"role_id\": \"4ccf0b65-fb99-40da-92d6-3f98730df8b2\",\n \"location_ids\": [\n \"a13ddb59-3e5c-4bdd-8673-fc913b93b9db\"\n ],\n \"first_name\": \"Jane\",\n \"last_name\": \"Smith\"\n }\n ],\n \"location_mode\": \"add\",\n \"notification\": \"none\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.xenia.team/api/v1/mgt/users/provision")
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 \"users\": [\n {\n \"email\": \"jane.smith@example.com\",\n \"role_id\": \"4ccf0b65-fb99-40da-92d6-3f98730df8b2\",\n \"location_ids\": [\n \"a13ddb59-3e5c-4bdd-8673-fc913b93b9db\"\n ],\n \"first_name\": \"Jane\",\n \"last_name\": \"Smith\"\n }\n ],\n \"location_mode\": \"add\",\n \"notification\": \"none\"\n}"
response = http.request(request)
puts response.read_body{
"status": true,
"results": [
{
"email": "jane.smith@example.com",
"status": "ok",
"user_id": "9c1e2f3a-1111-4b2c-8d3e-4f5a6b7c8d9e"
}
],
"errors": [
{
"email": "dupe@example.com",
"error": "Duplicate email in batch"
}
]
}Provision Users
Batch-creates or syncs users. New users are created with the given role and locations. Existing users (matched by email, case-insensitive) have their role corrected if it differs and locations updated per location_mode; re-provisioning does not change an existing membership’s status (a Pending/Deactivated user stays that way). Validation runs for the whole batch before any row is written; a bad role_id or location_id fails only that row, reported in errors. An email that appears more than once in the same batch fails every row that shares it, not just the extras.
By default no email is sent. Pass notification: "auto" to invite each newly created user, routed by their email domain — see the parameter description.
location_ids are expanded to their leaf locations before membership is written: passing a district or region grants the stores beneath it, because several read paths match a location literally rather than walking the hierarchy, and a user holding only a parent node would see empty Submissions and Tasks pages.
curl --request POST \
--url https://api.xenia.team/api/v1/mgt/users/provision \
--header 'Content-Type: application/json' \
--header 'x-client-key: <api-key>' \
--header 'x-client-secret: <api-key>' \
--data '
{
"users": [
{
"email": "jane.smith@example.com",
"role_id": "4ccf0b65-fb99-40da-92d6-3f98730df8b2",
"location_ids": [
"a13ddb59-3e5c-4bdd-8673-fc913b93b9db"
],
"first_name": "Jane",
"last_name": "Smith"
}
],
"location_mode": "add",
"notification": "none"
}
'import requests
url = "https://api.xenia.team/api/v1/mgt/users/provision"
payload = {
"users": [
{
"email": "jane.smith@example.com",
"role_id": "4ccf0b65-fb99-40da-92d6-3f98730df8b2",
"location_ids": ["a13ddb59-3e5c-4bdd-8673-fc913b93b9db"],
"first_name": "Jane",
"last_name": "Smith"
}
],
"location_mode": "add",
"notification": "none"
}
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({
users: [
{
email: 'jane.smith@example.com',
role_id: '4ccf0b65-fb99-40da-92d6-3f98730df8b2',
location_ids: ['a13ddb59-3e5c-4bdd-8673-fc913b93b9db'],
first_name: 'Jane',
last_name: 'Smith'
}
],
location_mode: 'add',
notification: 'none'
})
};
fetch('https://api.xenia.team/api/v1/mgt/users/provision', 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/users/provision",
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([
'users' => [
[
'email' => 'jane.smith@example.com',
'role_id' => '4ccf0b65-fb99-40da-92d6-3f98730df8b2',
'location_ids' => [
'a13ddb59-3e5c-4bdd-8673-fc913b93b9db'
],
'first_name' => 'Jane',
'last_name' => 'Smith'
]
],
'location_mode' => 'add',
'notification' => 'none'
]),
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/users/provision"
payload := strings.NewReader("{\n \"users\": [\n {\n \"email\": \"jane.smith@example.com\",\n \"role_id\": \"4ccf0b65-fb99-40da-92d6-3f98730df8b2\",\n \"location_ids\": [\n \"a13ddb59-3e5c-4bdd-8673-fc913b93b9db\"\n ],\n \"first_name\": \"Jane\",\n \"last_name\": \"Smith\"\n }\n ],\n \"location_mode\": \"add\",\n \"notification\": \"none\"\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/users/provision")
.header("x-client-key", "<api-key>")
.header("x-client-secret", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"users\": [\n {\n \"email\": \"jane.smith@example.com\",\n \"role_id\": \"4ccf0b65-fb99-40da-92d6-3f98730df8b2\",\n \"location_ids\": [\n \"a13ddb59-3e5c-4bdd-8673-fc913b93b9db\"\n ],\n \"first_name\": \"Jane\",\n \"last_name\": \"Smith\"\n }\n ],\n \"location_mode\": \"add\",\n \"notification\": \"none\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.xenia.team/api/v1/mgt/users/provision")
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 \"users\": [\n {\n \"email\": \"jane.smith@example.com\",\n \"role_id\": \"4ccf0b65-fb99-40da-92d6-3f98730df8b2\",\n \"location_ids\": [\n \"a13ddb59-3e5c-4bdd-8673-fc913b93b9db\"\n ],\n \"first_name\": \"Jane\",\n \"last_name\": \"Smith\"\n }\n ],\n \"location_mode\": \"add\",\n \"notification\": \"none\"\n}"
response = http.request(request)
puts response.read_body{
"status": true,
"results": [
{
"email": "jane.smith@example.com",
"status": "ok",
"user_id": "9c1e2f3a-1111-4b2c-8d3e-4f5a6b7c8d9e"
}
],
"errors": [
{
"email": "dupe@example.com",
"error": "Duplicate email in batch"
}
]
}hasPermission/hasFeature gate of its own — any valid client key can provision users into its own workspace. Pre-provisioning a user here with a role and locations before their first SSO login means they land in that role and those locations instead of the workspace’s bare default SSO role. See the Single Sign-On guide.role_id/location_ids values? See Get Roles and Get Locations.{ status, results, errors } — not the { data, extra_meta } shape used by other user-management endpoints. A 200 response does not mean every row succeeded: always check errors. A duplicated email within the same batch fails every row that shares it, not just the extra ones.Pending or Deactivated user stays that way. It also doesn’t bypass your workspace’s user-seat limit: if provisioning a new user would exceed it, that row fails in errors.notification defaults to none, so an integration already live on this endpoint keeps creating users silently. Pass notification: "auto" to invite each newly created user, routed by their email domain: an address on one of the workspace’s verified SSO domains gets an Active account with no password and a sign-in link, everyone else gets a Pending account and the standard invite email where they set their own password.Users who already existed are never emailed, whatever you pass — re-provisioning is a sync, not an onboarding event, and the alternative is re-mailing your whole directory every time the integration runs.notification: "auto" requires the API key to have a default user set, because both emails are sent on a named person’s behalf (“X has invited you to Xenia”). Without one, the entire batch is rejected before anything is written, rather than failing partway with some users created and unnotified. notification: "none" has no such requirement.location_ids are expanded to their leaf locations before membership is written, so passing a district or region grants the stores beneath it. This matters because membership is stored flat and several read paths match a location literally rather than walking the hierarchy — a user holding only a parent node would sign in to empty Submissions and Tasks pages. Passing leaf locations directly is unaffected.Authorizations
Client API key for authentication
Client secret for authentication
Body
1Show child attributes
Show child attributes
add (default) only adds missing location memberships, never removes. replace makes the user's locations exactly match location_ids, removing any not listed.
add, replace Whether newly created users are emailed. none (default) creates them silently, which is what every integration already live on this endpoint does. auto notifies each NEWLY created user and routes on their email domain: an address on one of the workspace's verified SSO domains gets an Active account with no password plus a sign-in link, and every other address gets a Pending account plus the standard invite email where they set their own password. Users who already existed are never emailed — re-provisioning is a sync, not an onboarding event. Requires the API key to have a default user set, since notifications are sent on a named person's behalf; without one the whole batch is rejected before anything is written.
none, auto