# Totalum App Builder API — Project Groups

This file is the complete reference for this area. The short guide for agents is https://www.totalum.app/totalum-api.md.

- Base URL: `https://builder-api.totalum.app/api/v1/vcaas`
- Authentication: header `api-key: tlm_sk_…` on every request, from a server only.
- Response envelope: `{ "errors": null | { "errorCode", "errorMessage", "errorDetails?" }, "data": … }`.


---

# Project Groups

> Optional folders for organising projects — additive, and never required.

Project groups are **optional folders** for projects. Nothing in the API requires one: a project with no group behaves exactly as every project did before groups existed, and filing one away never hides it from `GET /projects`. All endpoints require the `api-key` header and are **free**.

A project joins a group through `groupId` on [Launch Project](https://www.totalum.app/docs/api/projects#launch-project) or [Update Project](https://www.totalum.app/docs/api/projects#update-project), and you filter by group with `GET /projects?groupId=<id>` (or `?groupId=none` for the ungrouped ones).

:::info Membership lives on the project
A group is a label, not a container: the link is stored on the project, not as a list of project ids on the group. That is why deleting a group is a cheap operation that releases its members, and why a group can hold 100,000 projects without any document growing.
:::

:::warning A group belongs to one account
Every lookup is scoped to the calling account, and a group owned by somebody else is indistinguishable from one that does not exist — you get `404 PROJECT_GROUP_NOT_FOUND` either way.
:::

**Limits:** 3,000 groups per account · 100,000 projects per group · name up to 80 characters · description up to 500 characters.

## Create Project Group

```endpoint
method: POST
path: /api/v1/vcaas/project-groups
cost: Free
```

Create a folder for projects.

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Trimmed, up to 80 characters. Required — an empty name is rejected |
| `description` | string | No | Trimmed and **truncated** to 500 characters rather than rejected |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.groupId` | string | The new group's ID — use this as `groupId` elsewhere |
| `data.name` | string | The group name |
| `data.description` | string | The description, or an empty string when none was given |
| `data.createdAt` | string | ISO 8601 creation date |
| `data.updatedAt` | string | ISO 8601 last-modified date |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"name":"Client work","description":"Everything billed to clients"}' \
  https://builder-api.totalum.app/api/v1/vcaas/project-groups
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "groupId": "65f1a2b3c4d5e6f7a8b9c0d1",
    "name": "Client work",
    "description": "Everything billed to clients",
    "createdAt": "2026-08-04T10:00:00.000Z",
    "updatedAt": "2026-08-04T10:00:00.000Z"
  }
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "CREATE_PROJECT_GROUP_ERROR",
    "errorMessage": "Group name is required"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `CREATE_PROJECT_GROUP_ERROR` | 400 | Name missing or longer than 80 characters, or the account already has 3,000 groups |

## List Project Groups

```endpoint
method: GET
path: /api/v1/vcaas/project-groups
cost: Free
```

Your groups, **newest first**, each with the number of projects currently filed under it.

**Query parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `search` | string | No | Case-insensitive match on the group **name** only |
| `limit` | number | No | Page size, default 100, maximum 200 |
| `skip` | number | No | Groups to skip, for paging (default: 0) |

**Response headers**

| Header | Type | Description |
|---|---|---|
| `X-Total-Count` | number | Total groups matching the filters, across all pages |
| `X-Limit` | number | Page size actually applied |
| `X-Skip` | number | Offset actually applied |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data` | array | Array of group objects, newest first |
| `data[].groupId` | string | The group ID |
| `data[].name` | string | The group name |
| `data[].description` | string | The description, or an empty string |
| `data[].createdAt` | string | ISO 8601 creation date |
| `data[].updatedAt` | string | ISO 8601 last-modified date |
| `data[].projectCount` | number | How many of your projects are filed under this group |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  "https://builder-api.totalum.app/api/v1/vcaas/project-groups?limit=100"
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": [
    {
      "groupId": "65f1a2b3c4d5e6f7a8b9c0d1",
      "name": "Client work",
      "description": "Everything billed to clients",
      "createdAt": "2026-08-04T10:00:00.000Z",
      "updatedAt": "2026-08-04T10:00:00.000Z",
      "projectCount": 12
    }
  ]
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "LIST_PROJECT_GROUPS_ERROR",
    "errorMessage": "Error listing project groups"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `LIST_PROJECT_GROUPS_ERROR` | 400 | Internal error listing project groups |

## Get Project Group

```endpoint
method: GET
path: /api/v1/vcaas/project-groups/:groupId
cost: Free
```

One group, with its current project count.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `groupId` | string | The group ID |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.groupId` | string | The group ID |
| `data.name` | string | The group name |
| `data.description` | string | The description, or an empty string |
| `data.createdAt` | string | ISO 8601 creation date |
| `data.updatedAt` | string | ISO 8601 last-modified date |
| `data.projectCount` | number | How many of your projects are filed under this group |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/project-groups/65f1a2b3c4d5e6f7a8b9c0d1
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "groupId": "65f1a2b3c4d5e6f7a8b9c0d1",
    "name": "Client work",
    "description": "",
    "createdAt": "2026-08-04T10:00:00.000Z",
    "updatedAt": "2026-08-04T10:00:00.000Z",
    "projectCount": 12
  }
}
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_GROUP_NOT_FOUND",
    "errorMessage": "Project group not found"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `PROJECT_GROUP_NOT_FOUND` | 404 | No such group, the id is malformed, or it is not yours |
| `GET_PROJECT_GROUP_ERROR` | 400 | Internal error getting the project group |

## Update Project Group

```endpoint
method: PATCH
path: /api/v1/vcaas/project-groups/:groupId
cost: Free
```

Rename a group or change its description. Keys you do not send are left alone, so renaming a group cannot blank its description.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `groupId` | string | The group ID |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | No | Trimmed, up to 80 characters. Sending an empty name is an error — omit the key instead |
| `description` | string | No | Trimmed and truncated to 500 characters. Send an empty string to clear it |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.groupId` | string | The group ID |
| `data.name` | string | The name after the update |
| `data.description` | string | The description after the update |
| `data.createdAt` | string | ISO 8601 creation date |
| `data.updatedAt` | string | ISO 8601 last-modified date |
| `data.projectCount` | number | How many of your projects are filed under this group |

**Example request**

```bash
curl -X PATCH \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"name":"Client work 2026"}' \
  https://builder-api.totalum.app/api/v1/vcaas/project-groups/65f1a2b3c4d5e6f7a8b9c0d1
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "groupId": "65f1a2b3c4d5e6f7a8b9c0d1",
    "name": "Client work 2026",
    "description": "Everything billed to clients",
    "createdAt": "2026-08-04T10:00:00.000Z",
    "updatedAt": "2026-08-04T12:30:00.000Z",
    "projectCount": 12
  }
}
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_GROUP_NOT_FOUND",
    "errorMessage": "Project group not found"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `PROJECT_GROUP_NOT_FOUND` | 404 | No such group, the id is malformed, or it is not yours |
| `UPDATE_PROJECT_GROUP_ERROR` | 400 | Empty name, name longer than 80 characters, or an internal error |

## Delete Project Group

```endpoint
method: DELETE
path: /api/v1/vcaas/project-groups/:groupId
cost: Free
```

Delete the **folder only**.

:::warning Deleting a group never deletes a project
Its members simply become ungrouped — the same state every project is in by default — and `releasedProjects` tells you how many were released. There is deliberately no cascade: a folder is not an owner. To delete the projects themselves, call [Delete Project](https://www.totalum.app/docs/api/projects#delete-project) for each one.
:::

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `groupId` | string | The group ID |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.deleted` | boolean | `true` on successful deletion |
| `data.releasedProjects` | number | How many projects became ungrouped. None were deleted |

**Example request**

```bash
curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/project-groups/65f1a2b3c4d5e6f7a8b9c0d1
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "deleted": true, "releasedProjects": 12 } }
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_GROUP_NOT_FOUND",
    "errorMessage": "Project group not found"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `PROJECT_GROUP_NOT_FOUND` | 404 | No such group, the id is malformed, or it is not yours |
| `DELETE_PROJECT_GROUP_ERROR` | 400 | Internal error deleting the project group |
