Users and divisions
Reading needs users:read or divisions:read. Writing needs users:write or divisions:write.
They are separate scopes on purpose: a directory sync should not be one credential away from
creating accounts. Check what you hold with GET /v1/me.
| Endpoint | Scope |
|---|---|
GET /v1/users · GET /v1/users/{userId} |
users:read |
POST /v1/users |
users:write |
PATCH /v1/users/{userId} |
users:write |
PUT /v1/users/{userId}/segments |
users:write |
GET /v1/divisions · GET /v1/divisions/{divisionId} |
divisions:read |
POST /v1/divisions |
divisions:write |
PATCH /v1/divisions/{divisionId} |
divisions:write - erases the address, see below |
Read this first
Section titled “Read this first”Five behaviors that are not obvious from the endpoint list, and that you will otherwise learn the hard way.
1. Creating a user does not send a usable invitation
Section titled “1. Creating a user does not send a usable invitation”You do not supply a password, and you never receive one - this API generates a random one it does not reveal, log, or store anywhere you can read.
Collaterate does send its “Thank You for Registering” email on creation. That email contains no reset link. So the new user cannot act on it. They sign in for the first time through the storefront’s forgot-password page.
Tell your users that, or the invite will confuse them. If you send your own welcome message, point it at forgot-password rather than at the Collaterate email.
2. Your changes are attributed to us, not to you
Section titled “2. Your changes are attributed to us, not to you”Collaterate stamps its own updated_by column with the shared service account this API
authenticates as. There is no on-behalf-of on any of these endpoints, so a site owner reading the
admin UI’s audit column cannot tell which partner made a change - or tell a partner’s change from a
staff member’s.
Per-partner attribution exists, but it lives in this API’s audit trail: partner, endpoint, fields attempted, outcome, for every mutating request. Ask us if you need a change traced.
3. Assigning segments turns allSegmentsVisible off
Section titled “3. Assigning segments turns allSegmentsVisible off”Always. Even if the segment list does not change. That is Collaterate’s behavior and this API cannot prevent it.
If the user currently has allSegmentsVisible: true, the request is refused until you resend with
"acknowledgeAllSegmentsVisibleReset": true. Flipping a user from “sees every segment” to “sees
only these” changes what they see in the storefront, and it should not happen by accident.
4. A write response is a fresh read, not an echo
Section titled “4. A write response is a fresh read, not an echo”Every write here returns the record re-read afterwards, never the upstream reply. Under rare replication lag the body can briefly show the previous values even though the write applied. Read again if it matters; do not retry the write.
5. Storefront changes may lag
Section titled “5. Storefront changes may lag”Nothing downstream is notified when segment assignments change - no search reindex, no cache eviction. A storefront can serve the previous assignment for a short time. That is a lag, not a failed write. This API’s own read is the authority.
Listing and reading
Section titled “Listing and reading”GET /v1/users is keyset-paginated - walk nextCursor until it is null. Two optional
filters narrow it: siteId restricts to one granted site, and divisionId restricts to one
division. A siteId outside your grant is a 400 invalid_site_id, never an empty page.
List the users on one site:
GET /v1/sites/222/usersThis is exactly GET /v1/users?siteId=222 - same handler, same result. Use whichever
reads better in your code. To reconcile every user you can see, page the unfiltered
GET /v1/users: with 419,979 users across 475 sites (251,293 on site 1 alone), one
paged sweep beats one call per site.
List the users under one division the same way:
GET /v1/divisions/4210/usersThis is exactly GET /v1/users?divisionId=4210 - same handler, same result.
Deactivated users are listed, with active: false. A user who was deactivated and one who
never existed are different facts, and hiding the first would make them look identical.
GET /v1/users/{userId} adds a segments array. Only segments belonging to the user’s own site
appear there.
Creating
Section titled “Creating”POST /v1/usersContent-Type: application/json
{ "siteId": 518, "username": "jane.doe@example.com", "email": "jane.doe@example.com", "firstName": "Jane", "lastName": "Doe", "company": "Acme", "divisionId": 4211 }Five required fields, plus company, phone and divisionId if you have them. Omit an
optional field to leave it unset - null is refused here, because on a create there is no previous
value to clear and omission already says “nothing for this”. (PATCH does accept null on those
three, where it means something.)
A divisionId must belong to the same site. One that does not is refused with
422 division_not_on_site and nothing is created. That check is this API’s own: the platform
resolves the id without comparing sites, so without it you could attach a new user to another
site’s division.
username is the sign-in identifier and is unique per site, case-insensitively. It cannot be
changed afterwards.
The response is a receipt - 201 {"userId": 991} with a Location header - not the user. Fetch
GET /v1/users/{userId} for the full record, including the active value the platform defaulted
it to.
Changing
Section titled “Changing”PATCH /v1/users/440231Content-Type: application/json
{ "active": false }Seven fields: active, email, firstName, lastName, company, phone, divisionId. Omitted
fields are unchanged. username is not writable.
Nulls are asymmetric. company, phone and divisionId accept null - clearing a division
assignment is a real operation. email, firstName, lastName and active do not: a null clears
the field, and an account with no email cannot use the forgot-password flow, which is the only
way a user you created ever gets in. Those four answer 400 null_not_supported.
A divisionId must be a division on that user’s own site. One that is not - whether it does not
exist or belongs elsewhere - answers 422 division_not_on_site.
Segments
Section titled “Segments”PUT /v1/users/440231/segmentsContent-Type: application/json
{ "segmentIds": [91, 92] }Every id must be a segment on the user’s own site. One that is not answers
422 segment_not_on_site, identically whether it does not exist or belongs to another site, and
nothing is changed.
Segments assigned automatically by a site rule are protected. If your list would drop one, the
request is refused with 409 auto_assigned_segments_present naming the ids. Include them to keep
them.
One thing worth knowing rather than discovering: a user can carry a link to a segment on a different site. Those links are invisible to this API - they name a segment you are not entitled to see - and a replace removes them. That is a cleanup, not the loss of anything this API published.
Divisions
Section titled “Divisions”Divisions form a tree. This API publishes them flat, with parentId links, so one page is one
page. A division’s children are the divisions whose parentId is its id.
An empty list is a normal answer. Sites that do not use divisions return
{"divisions": [], "nextCursor": null} - a success, not an error.
Two optional filters narrow the list: siteId restricts to one granted site, and parentId
restricts to one division’s direct children. A siteId outside your grant is a
400 invalid_site_id, never an empty page.
List the divisions on one site:
GET /v1/sites/222/divisionsThis is exactly GET /v1/divisions?siteId=222 - same handler, same result. Use whichever
reads better in your code.
Creating
Section titled “Creating”POST /v1/divisionsContent-Type: application/json
{ "siteId": 518, "name": "Northeast Region", "code": "NE-01" }The supplier and country come from the site record and cannot be supplied. A new division is always
created active - an inactive one cannot be assigned to a user. Nesting via parentId is limited
to 10 levels, and the parent must be on the same site.
code should be unique on the site, and this API checks before creating - a duplicate answers
409 division_code_conflict. Codes are used to match users at sign-in, so a duplicate makes that
match ambiguous.
Changing
Section titled “Changing”PATCH /v1/divisions/{divisionId} changes a division’s name, code, parentId or active.
Send only the fields you want to change; an omitted field is left alone. parentId accepts null,
which detaches the division and makes it a root.
An empty body is 400 invalid_request rather than a no-op, because upstream even an empty
update clears the address - a request that changes nothing must not be able to destroy something.
The response is re-read from our database after the write lands, so it reflects what was actually stored rather than echoing your request.
Over MCP
Section titled “Over MCP”The four read operations - listUsers, getUser, listDivisions, getDivision - are available as
MCP tools.
Creating a user, creating a division and updating a division are not, deliberately. Creating a user mints a credentialed storefront account. Updating a division erases its address on most sites, which is not something an agent should be able to do from a misread instruction. These are the least reversible calls in this surface, and none of them is something you would easily notice or undo. They are REST-and-SDK only.
Errors
Section titled “Errors”See Errors for the full table. The ones specific to this surface:
code |
Status | Meaning |
|---|---|---|
invalid_user_id · invalid_division_id |
400 | Path segment is not a positive whole number of at most nine digits |
division_create_requires_address |
400 | That site stores an address on every division - see above |
user_not_found · division_not_found |
404 | No such record on your granted sites |
user_already_exists |
409 | Username or email already taken on that site |
division_code_conflict |
409 | That code is already in use on the site |
all_segments_visible_reset_not_acknowledged |
409 | Resend with the acknowledgement |
auto_assigned_segments_present |
409 | Your list would drop a rule-assigned segment |
division_not_on_site · segment_not_on_site |
422 | The id belongs to another site, or does not exist |
parent_not_on_site |
422 | The parentId you sent is not a division on that division’s own site |
parent_is_self |
422 | You asked to make a division its own parent |
audit_unavailable |
503 | We could not record the attempt, so we did not make it. Retry. |