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, a key, 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 key to 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.

Authentication How to obtain and send an OAuth2 access token.

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: GET on the collection returns that one table with its columns and items nested, 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:

  • Name never 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, joinedAt and isExternal in 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 — users here, departments for the Departments surface, locations for the Locations surface, assets for the Assets surface, lists for 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 typelocation, 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 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/columns
  • GET / 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/items
  • GET / 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.