Connected Workers Users
The Connected Workers Users API lets you push the people of a workspace — your roster — and carry your own fields about them. A user is a lightweight record keyed by Email: it names a person, places them in the organization, and holds any custom columns you add.
- Users table — a table. Has a
name, akey, and rows. One per workspace, created with the workspace. - Column — a field definition on the table (name, key, data type, ordering, display behavior). The Users table arrives with its system columns already seeded; you add custom columns alongside them.
- Item — a row: an object mapping each column's
keyto its value ({ "email": "dana.levi@example.com", "employeeNo": "10442" }). A person is that row, identified by their Email.
Workspace 7
└── Users table 21 "Users" (key: users)
├── Columns: Name (title) · Email (primary key) · Role · Status ·
│ Department (reference) · Teams · Tour access ·
│ Invited by · Joined · External ← system, seeded
│ Employee no · Work center ← your custom columns
└── Items (rows):
{ "id": 3001, "name": "Dana Levi", "email": "dana.levi@example.com", "employeeNo": "10442" }
{ "id": 3002, "name": "Amir Cohen", "email": "amir.cohen@example.com", "employeeNo": "10443" }
Users are record-native, like locations and departments: there is effectively one Users table per workspace, and a person is a record identified by their Email (its isPrimaryKey column). See the Departments guide, the Locations guide, the Assets guide, and the Lists guide for the other CW table entities.
A row is a real person. Landing an email in this table registers a Treedis account and invites the person, or — if the address is already known — grants them membership of this workspace. That is what makes the Users table different from every other CW table: it has real-world side effects. A person keeps one account across every workspace they work in.
Base URL & auth
Every endpoint is scoped to a workspace and served under:
https://api.treedis.com/v2/api/workspaces/{workspaceId}/cw/users
All endpoints require authentication (OAuth2 Bearer token or API key), and the caller must have access to the workspace. workspaceId is always a numeric ID. An API key is bound to a single workspace and can only address its own. The workspace must be a Connected Workers workspace — otherwise every endpoint here returns 400.
Keys — address tables and columns by id or slug
Tables and columns each have a key — a URL‑safe slug (lowercase, no spaces), generated automatically from the name on create (or passed). Keys are unique within their scope and never all‑numeric.
- Anywhere a path takes
{columnId}, pass either the numeric id or the key (e.g.…/cw/users/columns/email). A purely numeric value is an id; anything else is a key. The Users table itself is not addressed by id — it is the single per-workspace table reached directly under…/cw/users. - Rows reference columns by key — the property names in a row object are column keys.
- Items (rows) have no key — address them by their numeric
id. Their business key is the Email.
Endpoints
Users is a single per-workspace table. Like locations and departments (and unlike assets and lists, where you manage many tables), every workspace has one Users table — it is record-native. There is no table-id segment:
GETon the collection returns that one table with itscolumnsanditemsnested, and every sub-resource hangs directly off…/cw/users(no{tableId}).
| Method | Path (under /v2/api/workspaces/{workspaceId}/cw/users) |
Purpose |
|---|---|---|
GET |
`` (collection) | Get the Users table (columns + items) |
POST |
`` (collection) | Update the table's own key |
POST |
/bulk |
Push people (register / invite / merge) |
POST |
/csv-import/presign · /csv-import |
Large roster import from CSV |
GET |
/csv-import/{jobId} |
CSV import job status |
GET / POST |
/columns |
List columns / create a custom column |
GET / PUT / DELETE |
/columns/{columnId} |
Get / update / delete a column |
GET |
/items |
List people |
GET / PUT |
/items/{itemId} |
Get a person / update their custom values |
Two operations that exist on the other surfaces are deliberately refused here, because a user row is backed by a real account:
| Not supported | Why | Do this instead |
|---|---|---|
POST /items |
A user row cannot exist without an account behind it. | POST /bulk, or invite through the members flow. |
DELETE /items/{itemId} |
Removing a person also revokes their tour and project grants. | Remove them through the workspace members flow. |
Both return 400 with a message that says so. There is no /items/truncate for Users.
Pushing people
The one-shot call is POST /cw/users/bulk — it defines your custom columns and pushes the people in a single request. The envelope key is users. Email is the key; name seeds a newly registered account.
curl -X POST "https://api.treedis.com/v2/api/workspaces/7/cw/users/bulk" \
-H "Authorization: Bearer your-access-token" \
-H "Content-Type: application/json" \
-d '{
"users": [
{
"name": "Users", "key": "users", "status": "active",
"columns": [
{ "name": "Employee no", "type": "text" },
{ "name": "Work center", "type": "reference" }
],
"items": [
{ "name": "Dana Levi", "email": "dana.levi@example.com", "employeeNo": "10442", "workCenter": "MECH-01" },
{ "name": "Amir Cohen", "email": "amir.cohen@example.com", "employeeNo": "10443", "workCenter": "ELEC-02" }
]
}
]
}'
What happens per row:
| Incoming email | Result |
|---|---|
| No Treedis account | An account is registered and the person is invited to this workspace. |
| Has an account, not a member | Granted membership of this workspace — same account, no second identity. |
| Already a member here | Their custom column values are merged. |
| Missing / empty | The row is skipped. |
Two rules keep the data owned by the right side:
Namenever overwrites an existing person's name. It seeds a brand-new account only — a person's name belongs to them, not to the organization pushing the file.- System columns keep their own writers.
role,status,department,teams,tourAccess,invitedBy,joinedAtandisExternalin the payload are ignored; they are set by the members / org endpoints. Only your custom columns are merged.
The response is the Users table (HTTP 201). Read the rows back with GET /cw/users/items to confirm what landed.
Each surface uses its own batch envelope key —
usershere,departmentsfor the Departments surface,locationsfor the Locations surface,assetsfor the Assets surface,listsfor the Lists surface.
References resolve by business key
A custom reference column arrives as the customer's own key — a department Name, a location code, a work-center name — not a Treedis row id. Each value is resolved to its target row, the same way the location and asset ingest work. A value that matches nothing is reported, not stored: a silently kept code would look like a working link in the grid.
Push departments and locations before users, so the targets exist when the references resolve.
Batch insert by type
One endpoint for every surface: POST /v2/api/workspaces/{workspaceId}/cw/bulk takes a tables array where each table carries its own type — location, asset, list, department, or user. Use type: user to push people through the unified call; it is equivalent to posting to /cw/users/bulk.
Tables are inserted in dependency order regardless of payload order — lists and departments first, then locations, then assets, and users last — so a person's department and location references resolve whatever order you send them in.
curl -X POST "https://api.treedis.com/v2/api/workspaces/7/cw/bulk" \
-H "Authorization: Bearer your-access-token" \
-H "Content-Type: application/json" \
-d '{
"tables": [
{
"type": "department",
"name": "Departments", "key": "departments", "status": "active",
"columns": [ { "name": "Name", "type": "text", "isTitle": true } ],
"items": [ { "name": "Maintenance" } ]
},
{
"type": "user",
"name": "Users", "key": "users", "status": "active",
"columns": [ { "name": "Employee no", "type": "text" } ],
"items": [
{ "name": "Dana Levi", "email": "dana.levi@example.com", "employeeNo": "10442" }
]
}
]
}'
Large rosters — CSV import
For a data set above a few thousand people, use the asynchronous CSV path instead of /bulk: presign an upload, PUT the file straight to S3, start the job, then poll it. Header columns are matched to column keys after slugifying (EMPLOYEE_NO matches employee_no), and rows are upserted set-based by Email, so re-running the same file converges.
# 1. presign
curl -X POST "https://api.treedis.com/v2/api/workspaces/7/cw/users/csv-import/presign" \
-H "Authorization: Bearer your-access-token" \
-H "Content-Type: application/json" \
-d '{ "fileName": "users.csv" }'
# 2. PUT the raw bytes to the returned uploadUrl, then start the job
curl -X POST "https://api.treedis.com/v2/api/workspaces/7/cw/users/csv-import" \
-H "Authorization: Bearer your-access-token" \
-H "Content-Type: application/json" \
-d '{ "s3Key": "cw-imports/7/8f3a-users.csv", "hasHeader": true }'
# 3. poll until state is succeeded or failed
curl "https://api.treedis.com/v2/api/workspaces/7/cw/users/csv-import/12345" \
-H "Authorization: Bearer your-access-token"
Users are record-native, so no tableKey is needed. When rows fail, the job's result.errorReportKey is the S3 key of a CSV listing them. See the CSV Import guide for the full flow.
Columns
The Users table arrives with its system columns already seeded:
| Key | Name | Type | Notes |
|---|---|---|---|
name |
Name | text | The row's display title. Account identity. |
email |
text | Primary key — unique per account. | |
role |
Role | text | Set by the members / role endpoints. |
status |
Status | text | Membership status. |
department |
Department | reference | Set by the org endpoints. |
teams |
Teams | text | Set by the org endpoints. |
tourAccess |
Tour access | text | Set by the access endpoints. |
invitedBy |
Invited by | text | Who sent the invitation. |
joinedAt |
Joined | date | When they joined. |
isExternal |
External | checkbox | Whether they are an external collaborator. |
A system column carries isSystem: true. That means: it cannot be deleted, its type is locked, and its values are read-only here — each stays written by the one flow that owns it. You may still rename it and hide it (isVisible: false), and you may add any number of custom columns alongside.
GET/POST/v2/api/workspaces/{workspaceId}/cw/users/columnsGET/PUT/DELETE…/columns/{columnId}
| Field | Type | Notes |
|---|---|---|
name |
string | Required. Column name — unique within the table. |
key |
string | URL‑safe slug; auto‑generated from name if omitted. Used as the property name in rows. |
type |
enum | Required. See the column types below. |
order |
number | Display order. |
isRequired |
boolean | Whether a value is required. |
settings |
object | Type‑specific settings (e.g. maxLength, pattern). |
status |
active | inactive |
Column status. |
isVisible |
boolean | Whether the column shows in the UI. |
isPrimaryKey cannot be set on the Users table — Email is its primary key and stays so. isTitle likewise stays on the seeded Name column.
Column types (type): text · number · date · checkbox · hierarchyParent (links a row to a parent row) · reference (links to another table/entity) · locationTag (ties the value to a tour location/tag) · space (a space/environment) · tagStatus (a status tag).
curl -X POST "https://api.treedis.com/v2/api/workspaces/7/cw/users/columns" \
-H "Authorization: Bearer your-access-token" \
-H "Content-Type: application/json" \
-d '{ "name": "Employee no", "type": "text", "status": "active", "isVisible": true }'
Items (rows)
An item is a row — an object mapping each column's key to its value. A person's row is identified by their email. Responses add the row id.
GET/v2/api/workspaces/{workspaceId}/cw/users/itemsGET/PUT…/items/{itemId}
curl "https://api.treedis.com/v2/api/workspaces/7/cw/users/items?filter[email]=dana.levi@example.com" \
-H "Authorization: Bearer your-access-token"
{
"success": true,
"code": 200,
"message": "Success",
"data": [
{
"id": 3001,
"name": "Dana Levi",
"email": "dana.levi@example.com",
"role": "worker",
"status": "active",
"department": "Maintenance",
"employeeNo": "10442"
}
]
}
List rows with ?filter[<columnKey>]=<value>, ?search, ?page / ?limit, and ?sortBy / ?sortOrder. Active members come first, then pending invitations.
A PUT on …/items/{itemId} updates the row's custom values; send only the columns you want to change. System fields in the payload are ignored.
curl -X PUT "https://api.treedis.com/v2/api/workspaces/7/cw/users/items/3001" \
-H "Authorization: Bearer your-access-token" \
-H "Content-Type: application/json" \
-d '{ "employeeNo": "20991" }'
Pending invitations
A person who was invited but has not accepted yet is also a row of this table — so a listing is the complete picture of who is in the workspace, not only who finished signing up. Such a row is read-only and carries only the facts the invitation knows (email, role, invited by); it has no department, no teams and no custom values yet. Its id is an encoded invitation id, not a membership id, so do not persist it as a stable person id — use the Email.
Errors
Errors use the same envelope with success: false and an optional details object:
{
"success": false,
"code": 400,
"message": "Remove a user through the workspace members flow, not by deleting a table row"
}
| Status | When |
|---|---|
400 |
Validation failed, an unresolvable key, not a Connected Workers workspace, or a refused operation (row create / row delete) |
403 |
The caller has no access to the workspace, or an API key addressing another workspace |
404 |
Column or item not found |
See the Connected Workers Users tag in the API Reference for the full request and response schemas.

