Mirror the enrollment tree into the placement tree
Requires a persisted company admin identity and members.manage (or users.update); droplet tokens are not supported. The path names the company’s placement (sponsor-role) tree. For a company whose placement tree uses mirror_enrollment and that has no legacy placement tree, it seats every enrolled member who has no placement seat under their enroller’s placement seat (a root mirrors a root), roots first, and re-parents an unpinned seat whose parent no longer matches; pinned seats are left alone. Member writes keep the trees mirrored from then on; this request backfills members seated before. It is idempotent: a second run reports every member unchanged. With dry_run false the mirror runs in the background: the response is 202 with a mirror run (queued), read back from GET mirror-runs/ until it is succeeded, failed or halted. A company has one active run at a time: a request while one is queued or running answers that run with started false. A run works in batches of up to 100 members, each in its own short tree lock that reads both tree definitions again. The enrollment tree is listed once per pass, outside the lock; each later pass takes only members enrolled or moved since, so a member enrolled during the run is mirrored. While the placement tree is still empty, a batch first checks that its max_depth covers the deepest enrollment chain (placement_too_shallow) and seats nothing otherwise. Once a batch seats a member the placement tree’s definition is frozen, so a member the placement engine refuses later (an enrollment chain deeper than the placement tree holds is refused max_depth_exceeded) is counted under failed and listed in failures, the batches already written stand, and the run ends halted (mirror_members_refused) rather than succeeded; a rerun finishes the mirror. A member a live enrollment write moved during the run is mirrored again. A run halts (mirror_window_limit) after twice the lock windows the enrollment seats at its start needed, so seats that keep arriving cannot keep it going. With dry_run true nothing is written and the counts say at once what would change, including members the run would refuse (counted under failed and listed in failures); a preview covers at most 5,000 enrollment seats (preview_too_large), and a real run has no seat limit. The company needs GENEALOGY_TREE_WRITES with both default trees authoritative (tree_writes_not_enabled otherwise); the rule must be mirror_enrollment (placement_not_mirrored), the company must not place from a legacy placement tree (legacy_placement_tree), the placement tree’s max_depth must be at least the enrollment tree’s and its deepest chain (placement_too_shallow). These are checked before a run is queued and again by the run. The example uses illustrative IDs.
curl -X POST 'https://api.fluid.app/api/company/v2026-10/genealogy/trees/7096ad49-7117-53f6-8f7f-4805473d14c5/mirror-enrollment' \
-H 'Authorization: Bearer <company-token>' \
-H 'Content-Type: application/json' \
--data '{"mirror":{"dry_run":true}}'import requests
url = "https://api.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment"
payload = { "mirror": { "dry_run": True } }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({mirror: {dry_run: true}})
};
fetch('https://api.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment', 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.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment",
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([
'mirror' => [
'dry_run' => true
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$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.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment"
payload := strings.NewReader("{\n \"mirror\": {\n \"dry_run\": true\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"mirror\": {\n \"dry_run\": true\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"mirror\": {\n \"dry_run\": true\n }\n}"
response = http.request(request)
puts response.read_body{
"status": 200,
"mirror": {
"tree_id": "7096ad49-7117-53f6-8f7f-4805473d14c5",
"dry_run": true,
"counts": {
"seated": 50,
"followed": 0,
"unchanged": 2,
"pinned": 0,
"parent_unseated": 0,
"failed": 0
},
"failures": []
},
"meta": {
"request_uuid": null,
"timestamp": "2026-10-04T00:00:00Z"
}
}{
"status": 202,
"started": true,
"mirror_run": {
"id": "01a08944-f646-7385-b8c7-a71782556f1a",
"tree_id": "7096ad49-7117-53f6-8f7f-4805473d14c5",
"status": "queued",
"counts": {
"seated": 0,
"followed": 0,
"unchanged": 0,
"pinned": 0,
"parent_unseated": 0,
"failed": 0
},
"failures": [],
"failure_type": null,
"failure_description": null,
"windows": 0,
"attempts": 0,
"requested_by_id": "c0dd6812-0ba6-56d2-879c-a36112f813c2",
"created_at": "2026-10-07T00:00:00.000000Z",
"started_at": null,
"finished_at": null
},
"meta": {
"request_uuid": null,
"timestamp": "2026-10-07T00:00:00Z"
}
}{
"message": "<string>",
"error": "<unknown>"
}{
"message": "<string>",
"error": "<unknown>"
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}Authorizations
Company admin or company service token, subject to developer permissions.
Path Parameters
UUID of the company's placement (sponsor-role) tree.
Body
Whether to preview or apply the mirror.
Show child attributes
Show child attributes
curl -X POST 'https://api.fluid.app/api/company/v2026-10/genealogy/trees/7096ad49-7117-53f6-8f7f-4805473d14c5/mirror-enrollment' \
-H 'Authorization: Bearer <company-token>' \
-H 'Content-Type: application/json' \
--data '{"mirror":{"dry_run":true}}'import requests
url = "https://api.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment"
payload = { "mirror": { "dry_run": True } }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({mirror: {dry_run: true}})
};
fetch('https://api.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment', 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.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment",
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([
'mirror' => [
'dry_run' => true
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$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.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment"
payload := strings.NewReader("{\n \"mirror\": {\n \"dry_run\": true\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"mirror\": {\n \"dry_run\": true\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/company/v2026-10/genealogy/trees/{tree_id}/mirror-enrollment")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"mirror\": {\n \"dry_run\": true\n }\n}"
response = http.request(request)
puts response.read_body{
"status": 200,
"mirror": {
"tree_id": "7096ad49-7117-53f6-8f7f-4805473d14c5",
"dry_run": true,
"counts": {
"seated": 50,
"followed": 0,
"unchanged": 2,
"pinned": 0,
"parent_unseated": 0,
"failed": 0
},
"failures": []
},
"meta": {
"request_uuid": null,
"timestamp": "2026-10-04T00:00:00Z"
}
}{
"status": 202,
"started": true,
"mirror_run": {
"id": "01a08944-f646-7385-b8c7-a71782556f1a",
"tree_id": "7096ad49-7117-53f6-8f7f-4805473d14c5",
"status": "queued",
"counts": {
"seated": 0,
"followed": 0,
"unchanged": 0,
"pinned": 0,
"parent_unseated": 0,
"failed": 0
},
"failures": [],
"failure_type": null,
"failure_description": null,
"windows": 0,
"attempts": 0,
"requested_by_id": "c0dd6812-0ba6-56d2-879c-a36112f813c2",
"created_at": "2026-10-07T00:00:00.000000Z",
"started_at": null,
"finished_at": null
},
"meta": {
"request_uuid": null,
"timestamp": "2026-10-07T00:00:00Z"
}
}{
"message": "<string>",
"error": "<unknown>"
}{
"message": "<string>",
"error": "<unknown>"
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}