# Users & workspace

A **workspace** is one company in TruTec. Every API key belongs to one workspace and sees only its data. These endpoints describe the workspace and the people in it, so your system can show the right company details, map TruTec user ids to your own staff records and onboard new members.

Billing is not available through the API: you can read the plan's name, but never payment details, and nothing here changes the plan.

To change the company profile, branding, estimating defaults, sales tax, job numbering and the other workspace settings, see [Settings](settings.md).

!!! note
    Partner keys add `firm_id` to name the workspace: in the query string, or in the JSON body on writes. See [Partner API](partner-api.md).

## Workspace

### Get the workspace

`GET /v1/workspace`

**Permission:** Read jobs

```bash
curl https://trutec.ai/api/v1/workspace \
  -H "X-API-Key: sk_your_api_key_here"
```

```json
{
  "data": {
    "id": 77,
    "name": "Northgate Paving",
    "plan": {"code": "gold", "name": "Gold"},
    "subscription_status": "active",
    "timezone": "America/Chicago",
    "address": "2200 Yard Rd, Austin, TX 78744",
    "website": "https://northgatepaving.example",
    "branding": {
      "logo_url": "https://media.example.com/logos/northgate.png?expires=1760285051&signature=...",
      "share_subdomain": "northgate"
    },
    "products": {
      "bid": true,
      "build": true,
      "aerial_takeoffs": true,
      "blueprint_takeoffs": true,
      "nearmap_imagery": true,
      "api": true,
      "landscape": false
    },
    "integrations": {"quickbooks": true, "hubspot": false, "jobber": false},
    "member_count": 14,
    "created_at": "2025-03-02T16:20:00Z"
  }
}
```

| Field | Description |
|---|---|
| `plan` | `code` and display `name` of the plan that sets the workspace's features. |
| `subscription_status` | `trialing`, `active`, `past_due`, `canceled` or `pending`. |
| `timezone` | IANA name. Schedules, timesheets and "today" are computed in it. |
| `address` | The office or yard address TruTec measures drive times from. `null` if not set. |
| `website` | The company website shown on map exports. |
| `branding.logo_url` | A time-limited link to the logo on proposals and exports. Download it rather than storing the link. |
| `branding.share_subdomain` | The custom subdomain on shared links, if set. |
| `products.bid` | Sales: CRM, takeoffs, estimating, proposals. Always on. |
| `products.build` | Operations: schedule and dispatch, crews, the crew app, timesheets, job costing. |
| `products.blueprint_takeoffs`, `nearmap_imagery`, `api` | Features the plan includes. |
| `products.landscape` | The workspace measures landscaping (lawn, beds, trees) instead of paving. |
| `integrations` | Which accounting and CRM connections are on. |

## Users

### List users

`GET /v1/users`

**Permission:** Any active key

The workspace's team members. See [List users](jobs.md#list-users) for the parameters and response.

### Get a user

`GET /v1/users/{user_id}`

**Permission:** Read jobs

One team member with their role in this workspace and their teams.

```bash
curl https://trutec.ai/api/v1/users/311 \
  -H "X-API-Key: sk_your_api_key_here"
```

```json
{
  "data": {
    "id": 311,
    "name": "Dana Ortiz",
    "username": "dana",
    "email": "dana@northgatepaving.example",
    "role": "member",
    "job_title": "Estimator",
    "phone_number": "+15125550142",
    "headshot_url": null,
    "is_firm_owner": false,
    "created_at": "2025-03-04T15:00:12.000000",
    "workspace_role": "member",
    "role_label": "Ops Manager",
    "custom_role": {"id": 6, "name": "Ops Manager"},
    "teams": [{"id": 3, "name": "North Crew"}],
    "active": true
  }
}
```

| Field | Description |
|---|---|
| `workspace_role` | `owner`, `member` or `crew` in this workspace. |
| `role_label` | What TruTec shows: Owner, Member, Crew, or the company role's name. |
| `custom_role` | The company-defined role the member holds, or `null`. |
| `teams` | Teams the person is on. |
| `active` | `false` when the person's access is suspended. |

A user who is not in this workspace returns `404 User not found`.

### List roles

`GET /v1/roles`

**Permission:** Read jobs

The built-in roles and the roles the company defined in **Settings › Team › Roles**, with what each company role is allowed to do and how many people hold it. Built-in roles have `permissions: null`: Owners can do everything, Members have the standard office access, Crew use the crew app only.

```json
{
  "data": [
    {"id": null, "key": "owner", "name": "Owner", "description": "Everything, always — billing, settings and everyone’s roles.", "built_in": true, "permissions": null, "member_count": 2},
    {"id": null, "key": "member", "name": "Member", "description": "Today’s office access. The per-person switches on Members still apply.", "built_in": true, "permissions": null, "member_count": 9},
    {"id": null, "key": "crew", "name": "Crew", "description": "The crew app only: their own work, time, photos and notes.", "built_in": true, "permissions": null, "member_count": 3},
    {"id": 6, "key": null, "name": "Ops Manager", "description": "Runs the schedule", "built_in": false,
     "permissions": [{"key": "view_pricing", "label": "See prices and costs", "group": "money"}], "member_count": 3}
  ]
}
```

Roles are assigned in TruTec, not through the API.

### List teams

`GET /v1/teams`

**Permission:** Read jobs

Teams are named groups of people (for example branches or crews) that jobs can be filed under: a job's `team_id`.

```json
{
  "data": [
    {"id": 3, "name": "North Crew", "color": "#2563eb", "members": [{"user_id": 311, "role": "lead"}], "created_at": "2026-02-01T14:00:00Z"}
  ]
}
```

## Invitations

### Invite a user

`POST /v1/users/invite`

**Permission:** Write jobs, with a key created by a workspace owner

!!! warning
    This sends an email. TruTec emails the address an invitation to join the workspace, from the same flow as **Settings › Members › Invite**. The link expires in 7 days.

| Name | Type | Required | Description |
|---|---|---|---|
| `email` | string | Yes | Who to invite. |
| `role` | string | No | `member` (the only value accepted). Owners are invited in TruTec. |

```bash
curl -X POST https://trutec.ai/api/v1/users/invite \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"email": "new.hire@northgatepaving.example"}'
```

Returns `201`:

```json
{
  "data": {
    "id": 418,
    "email": "new.hire@northgatepaving.example",
    "role": "member",
    "status": "pending",
    "invited_by": {"id": 9, "name": "Chris Lane"},
    "expires_at": "2026-10-19T15:04:11Z",
    "accepted_at": null,
    "created_at": "2026-10-12T15:04:11Z"
  },
  "created": true
}
```

Inviting an email that already has a pending invitation sends a fresh link and returns `200` with `"created": false`. The person joins when they sign up from the link; they then appear in [List users](#list-users).

| Status | `error` |
|---|---|
| 400 | `A valid email address is required`, `role must be member`, `Unknown fields` |
| 403 | `Owner key required`: the key was not created by an owner of this workspace |
| 409 | `Already a member`, with the member's `user_id` |

### List invitations

`GET /v1/invites`

**Permission:** Read jobs

Invitations, newest first, in the shape above. Filter with `status` (`pending`, `accepted`, `revoked`, `expired`); page with `limit` and `offset`. An invitation whose link ran out before it was used is returned with `status` `expired`.

### Revoke an invitation

`DELETE /v1/invites/{invite_id}`

**Permission:** Write jobs and Delete records, with a key created by a workspace owner

Revokes a pending invitation; its link stops working. Returns `204`. `409 Only pending invites can be revoked` for accepted ones.
