Skip to content

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

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.

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.

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/users

This 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/users

This 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.

POST /v1/users
Content-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.

PATCH /v1/users/440231
Content-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.

PUT /v1/users/440231/segments
Content-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 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/divisions

This is exactly GET /v1/divisions?siteId=222 - same handler, same result. Use whichever reads better in your code.

POST /v1/divisions
Content-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.

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.

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.

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.