> For the complete documentation index, see [llms.txt](https://docs.labatlas.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.labatlas.com/rest-api-reference.md).

# REST API Reference

The Lab Atlas public REST API lets external clients — scripts, integrations, internal tooling, and partner systems — interact with the same projects, studies, assays, and supporting resources that the Lab Atlas web client uses. This guide explains how to obtain credentials, authenticate requests, and call each endpoint.

{% hint style="info" %}
The Lab Atlas public API is currently available only to Enterprise customers.
{% endhint %}

{% hint style="warning" %}
The public API is currently in beta, some endpoints and their conetns may change over time.
{% endhint %}

### Quick start

```bash
# Set your tenant base URL and API key
export LA_BASE=https://your-tenant.labatlas.com
export LA_KEY=lak_xxxxxxxx.xxxxxxxxxxxxxxxxxxxx

# List the projects visible to your account
curl -s "$LA_BASE/api/beta/projects" \
     -H "Authorization: ApiKey $LA_KEY" | jq .

# Create a new project
curl -s -X POST "$LA_BASE/api/v1/projects" \
     -H "Authorization: ApiKey $LA_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "code": "MYPROJ",
       "name": "My Project",
       "description": "Created via the public API.",
       "visibility": "PRIVATE"
     }'
```

***

### Getting an API key

API keys are issued from inside the Lab Atlas web application. Each key is owned by a specific **organization user**, and every request made with that key executes under that user's permissions — there is no separate service account.

To create a key:

1. Sign in to your Lab Atlas tenant.
2. Open the user menu in the top-right corner and choose **Account settings**.
3. Select the **API keys** tab.
4. Click **Create new key**.
5. Give the key a memorable display name (for example, `lims-importer`, `nightly-backup`) and choose how long it should remain valid.
6. Copy the key value from the modal that appears. **This is the only time the full key value is shown.** Store it somewhere safe — a secret manager, your CI's environment variables, or an encrypted note.

You can also list, rotate, or revoke your existing keys from the same screen. Revoking a key takes effect immediately; any request presenting the revoked key will fail with `401 Unauthorized`.

> **Heads up:** the API-key UI is only available when your Lab Atlas tenant has the Enterprise feature flag enabled. If you do not see the **API keys** tab, contact your administrator.

A key value looks like `lak_<keyId>.<secret>`. The `lak_` prefix and `keyId` segment are stable identifiers; the segment after the dot is the secret material.

#### Per-user, per-organization

Each key is bound to exactly one `(user, organization)` pairing. If you belong to two organizations, you need a separate key for each. Switching your active organization in the web UI does not change the org a previously-issued key is scoped to.

#### Key lifecycle

| Action    | Result                                                                                                        |
| --------- | ------------------------------------------------------------------------------------------------------------- |
| Create    | Key is active and usable immediately                                                                          |
| View list | Shows key metadata only (display name, creation date, last-used time). Secret material is never re-displayed. |
| Rotate    | Revoke the old key and create a new one. There is no atomic rotation primitive.                               |
| Revoke    | Future requests with this key return `401 Unauthorized`. The key remains in the audit log.                    |
| Expire    | Keys expire automatically after their configured duration. Expired requests return `401`.                     |

***

### Authentication

Every request to `/api/beta/**` must present a valid API key. Two header formats are accepted; pick whichever fits your client best:

```http
Authorization: ApiKey lak_xxxxxxxx.xxxxxxxxxxxxxxxxxxxx
```

```http
X-API-KEY: lak_xxxxxxxx.xxxxxxxxxxxxxxxxxxxx
```

> **Do not use `Authorization: Bearer …`.** Bearer tokens are reserved for the internal `/api/i/**` API and will be rejected on the public endpoints.

Missing, expired, malformed, or revoked keys return `401 Unauthorized` with no response body. Keys that are valid but lack the permission required for the action return `403 Forbidden`.

#### Required transport

All requests must be made over HTTPS. The API enforces HSTS and will reject plain-HTTP traffic at the load balancer.

#### CORS

The public API is intended for server-to-server use. Cross-origin browser requests are not enabled by default. If you need to call the API from a browser-based application, deploy a thin server-side proxy in front of it.

***

### Permissions model

Because every key is bound to an organization user, authorization decisions follow the same rules used by the web UI:

* **Visibility** — `findAll` and `findById` endpoints only return resources the key's owning user can view. For projects this is determined by the project's visibility setting (`PUBLIC`, `PROTECTED`, `PRIVATE`) plus the user's role and team memberships.
* **Contribute** — `create`, `update`, `delete`, `status`, and `archive` endpoints require the user to be a project member (or higher). Calls that fail this check return `403 Forbidden`. Looking up a record the user cannot view returns `404 Not Found` instead — this is intentional and avoids leaking the existence of records the user has no business knowing about.
* **Admin-only operations** — Team writes and Assay-Type writes additionally require the `ORGANIZATION_ADMIN` role on the owning user. A non-admin key on these endpoints returns `403`.

If you revoke a user's admin role, every key they own immediately loses access to admin-only endpoints.

***

### Conventions

#### Base URL & versioning

| Mount           | Status                                                                                                        |
| --------------- | ------------------------------------------------------------------------------------------------------------- |
| `/api/beta/...` | Current. Use this.                                                                                            |
| `/api/v1/...`   | Once finalized, this will become the stable base of the version 1 API.                                        |
| `/api/i/...`    | Internal API used by the Lab Atlas web client. **Not for external use.** API keys are rejected on this mount. |

#### Content types

All requests and responses use `application/json` with UTF-8 encoding.

#### Resource identifiers

Identifiers are UUIDs (version 4), serialised as canonical lowercase strings with hyphens — e.g. `b1d1b15a-19e0-4d6f-8d33-7a3d3aef6cba`.

#### Dates

Dates are ISO 8601 strings in UTC. Both date-only (`2026-05-06`) and date-time (`2026-05-06T14:32:11.000+00:00`) formats are accepted on input.

#### Pagination

List endpoints return a page object compatible with Spring Data's `Page<T>` envelope:

```json
{
  "content": [
    { /* DTO */ },
    { /* DTO */ }
  ],
  "totalElements": 142,
  "totalPages": 8,
  "size": 20,
  "number": 0,
  "first": true,
  "last": false,
  "empty": false,
  "pageable": {
    "pageNumber": 0,
    "pageSize": 20,
    "sort": { /* ... */ },
    "offset": 0,
    "paged": true,
    "unpaged": false
  }
}
```

You can request a specific page and size using query parameters:

```
GET /api/beta/projects?page=2&size=50&sort=name,asc
```

| Param  | Default           | Notes                                                                             |
| ------ | ----------------- | --------------------------------------------------------------------------------- |
| `page` | `0`               | Zero-indexed.                                                                     |
| `size` | `20`              | Maximum permitted size depends on the resource; large pages may be capped at 200. |
| `sort` | resource-specific | `field,direction` pairs. Repeat the parameter to sort by multiple fields.         |

#### Status codes

| Code               | Meaning                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------ |
| `200 OK`           | Successful read or update. Response body present.                                                |
| `201 Created`      | New resource created. Body contains the created representation.                                  |
| `204 No Content`   | Successful delete. No body.                                                                      |
| `400 Bad Request`  | Malformed payload or invalid query parameter.                                                    |
| `401 Unauthorized` | Missing, expired, malformed, or revoked API key.                                                 |
| `403 Forbidden`    | Authenticated but lacking the required permission (e.g. not a contributor, or not an org admin). |
| `404 Not Found`    | Resource does not exist, or does exist but the key's owner cannot view it.                       |
| `409 Conflict`     | Concurrent update conflict or duplicate identifier.                                              |
| `5xx`              | Server-side error. Try again or contact support if it persists.                                  |

***

### Endpoint reference

The tables below list every public endpoint. Path-prefix is `https://<tenant>/api/beta`. Where an endpoint requires `ORGANIZATION_ADMIN`, this is noted in the right-most column.

#### Projects

Projects are the top-level container for studies and assays.

| Method | Path                                          | Description                                          | Auth        |
| ------ | --------------------------------------------- | ---------------------------------------------------- | ----------- |
| GET    | `/projects`                                   | Paginated list of projects visible to the caller.    | Member      |
| GET    | `/projects/{id}`                              | Fetch a single project.                              | Member      |
| POST   | `/projects`                                   | Create a new project.                                | Member      |
| PUT    | `/projects/{id}`                              | Update name, description, visibility, etc.           | Contributor |
| POST   | `/projects/{id}/status`                       | Change project status. Body: `{"status": "ACTIVE"}`. | Contributor |
| POST   | `/projects/{id}/archive?archived=true\|false` | Archive or restore.                                  | Contributor |
| DELETE | `/projects/{id}`                              | Delete the project (and its studies, assays).        | Contributor |

**Create example**

```bash
curl -X POST "$LA_BASE/api/beta/projects" \
     -H "Authorization: ApiKey $LA_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "code": "OPLAB",
       "name": "Operations Lab",
       "description": "Ops experiments for FY26.",
       "visibility": "PROTECTED",
       "status": "ACTIVE"
     }'
```

**Project status values**: `CREATED`, `INITIALIZING`, `INITIALIZATION_FAILED`, `ACTIVE`, `COMPLETE`, `ON_HOLD`, `DEACTIVATED`.

**Visibility values**: `PUBLIC`, `PROTECTED`, `PRIVATE`.

**Choosing storage (optional)**

By default a new project's files are provisioned in your organization's default cloud storage. To place the project's primary folder elsewhere, include an optional `storage` object on the create payload:

| Field     | Type   | Description                                                                                                                                                                                            |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `driveId` | UUID   | The storage drive to use (see `GET /storage-drives`).                                                                                                                                                  |
| `mode`    | enum   | `CREATE_NEW` — create a new folder under `path`; `USE_EXISTING` — attach the folder that already exists at `path`.                                                                                     |
| `path`    | string | For `CREATE_NEW`, the parent folder the new folder is created under; for `USE_EXISTING`, the folder itself. Ignored when `driveId` is the default cloud drive (the org's project root is always used). |
| `name`    | string | Optional folder name (`CREATE_NEW` only).                                                                                                                                                              |

Omit the `storage` object entirely to use the default cloud location. Each project has exactly one primary folder at creation; attach additional folders afterward via `POST /projects/{id}/storage-folders` (see Storage folders).

```bash
curl -X POST "$LA_BASE/api/beta/projects" \
     -H "Authorization: ApiKey $LA_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "code": "OPLAB",
       "name": "Operations Lab",
       "description": "Ops experiments for FY26.",
       "visibility": "PROTECTED",
       "storage": {
         "driveId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
         "mode": "CREATE_NEW",
         "path": "/projects",
         "name": "Operations Lab"
       }
     }'
```

**Choosing an ELN notebook (optional)**

If your organization has a connected Electronic Lab Notebook (ELN), you can link the new project to an existing notebook folder at creation time. Each platform has its own block; include the one matching your integration. Omit both to leave ELN provisioning off. The folder must already exist — the API links it as the project's primary notebook folder, it does not create a new one.

Benchling — `benchling` object:

| Field           | Type   | Description                                                            |
| --------------- | ------ | ---------------------------------------------------------------------- |
| `integrationId` | UUID   | The Benchling integration to use (see `GET /integrations`).            |
| `folderId`      | string | The Benchling folder to link as the project's primary notebook folder. |

CDD Vault — `cddVault` object (CDD Vault has no nested folders, so you link an existing project container):

| Field           | Type   | Description                                                 |
| --------------- | ------ | ----------------------------------------------------------- |
| `integrationId` | UUID   | The CDD Vault integration to use (see `GET /integrations`). |
| `projectId`     | number | The CDD Vault project (container) to link.                  |

```bash
curl -X POST "$LA_BASE/api/beta/projects" \
     -H "Authorization: ApiKey $LA_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "code": "OPLAB",
       "name": "Operations Lab",
       "description": "Ops experiments for FY26.",
       "visibility": "PROTECTED",
       "benchling": {
         "integrationId": "3f1a2b4c-5d6e-4f70-8a91-2b3c4d5e6f70",
         "folderId": "lib_abc123"
       }
     }'
```

**Choosing a default Git group (optional)**

If your organization has a connected GitLab integration, you can give the project a default Git group via a `defaultGitGroup` object on create/update. It is a convenience pre-fill only: when Git repositories are created for studies or assays under this project, this group is pre-selected — callers may always choose a different group, and **no repository is created for the project itself**. Omit it for no default.

| Field           | Type   | Description                                                                           |
| --------------- | ------ | ------------------------------------------------------------------------------------- |
| `integrationId` | UUID   | The GitLab integration the group belongs to (see `GET /integrations`).                |
| `groupId`       | string | The id of the Git group to use as the default, as it appears in your GitLab instance. |

A stored default that no longer resolves (group removed, integration disconnected) is simply ignored when pre-filling — it never blocks repository creation.

***

#### Studies

Studies belong to projects and group together related experiments.

| Method | Path                                         | Description                                                | Auth                          |
| ------ | -------------------------------------------- | ---------------------------------------------------------- | ----------------------------- |
| GET    | `/studies`                                   | Paginated list of studies visible to the caller.           | Member                        |
| GET    | `/studies/{id}`                              | Fetch a single study.                                      | Member                        |
| POST   | `/studies`                                   | Create a new study under a project.                        | Contributor on parent project |
| PUT    | `/studies/{id}`                              | Update study fields.                                       | Contributor                   |
| POST   | `/studies/{id}/status`                       | Change status. Body: `{"status": "COMPLETE"}`.             | Contributor                   |
| POST   | `/studies/{id}/archive?archived=true\|false` | Archive or restore.                                        | Contributor                   |
| PATCH  | `/studies/{id}/move`                         | Move to a different project. Body: `{"projectId": "..."}`. | Contributor on both projects  |
| DELETE | `/studies/{id}`                              | Delete the study.                                          | Contributor                   |

**Create example**

```bash
curl -X POST "$LA_BASE/api/beta/studies" \
     -H "Authorization: ApiKey $LA_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "Cell viability screen",
       "description": "12-point dose response.",
       "projectId": "b1d1b15a-19e0-4d6f-8d33-7a3d3aef6cba",
       "status": "ACTIVE",
       "startDate": "2026-05-10"
     }'
```

**Study status values**: `CREATED`, `INITIALIZING`, `INITIALIZATION_FAILED`, `ACTIVE`, `COMPLETE`, `ON_HOLD`, `DEACTIVATED`.

**Choosing an ELN notebook (optional)**

If the parent project is linked to an ELN, you can provision notebook content for the study at creation time. Unlike a project (which links an existing folder), a study inherits the project's ELN integration and gets its own child content. Omit both fields to leave ELN provisioning off.

Benchling — `benchling` object: creates a notebook folder under the project's Benchling folder and a notebook entry in it.

| Field            | Type   | Description                                                                |
| ---------------- | ------ | -------------------------------------------------------------------------- |
| `templateId`     | string | Optional Benchling entry template to apply. Omit for a blank entry.        |
| `templateFields` | object | Optional field values for the chosen template (`{ "Field name": value }`). |

CDD Vault — `useCddVault` boolean: when `true`, creates a notebook entry under the project's CDD Vault container. CDD Vault has no nested folders, so there is nothing else to configure per study.

```bash
curl -X POST "$LA_BASE/api/beta/studies" \
     -H "Authorization: ApiKey $LA_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "Cell viability screen",
       "description": "12-point dose response.",
       "projectId": "b1d1b15a-19e0-4d6f-8d33-7a3d3aef6cba",
       "benchling": {
         "templateId": "tmpl_abc123",
         "templateFields": { "Owner": "Jane Doe" }
       },
       "useCddVault": false
     }'
```

***

#### Assays

Assays are the lowest-level experimental unit; each assay belongs to one study and has a defined assay type.

| Method | Path                                        | Description                                                                      | Auth                        |
| ------ | ------------------------------------------- | -------------------------------------------------------------------------------- | --------------------------- |
| GET    | `/assays`                                   | Paginated list of assays visible to the caller.                                  | Member                      |
| GET    | `/assays/{id}`                              | Fetch a single assay.                                                            | Member                      |
| POST   | `/assays`                                   | Create a new assay under a study. Body must include `studyId` and `assayTypeId`. | Contributor on parent study |
| PUT    | `/assays/{id}`                              | Update assay fields.                                                             | Contributor                 |
| POST   | `/assays/{id}/status`                       | Change status.                                                                   | Contributor                 |
| POST   | `/assays/{id}/archive?archived=true\|false` | Archive or restore.                                                              | Contributor                 |
| PATCH  | `/assays/{id}/move`                         | Move to a different study. Body: `{"studyId": "..."}`.                           | Contributor on both studies |
| DELETE | `/assays/{id}`                              | Delete the assay.                                                                | Contributor                 |

**Create example**

```bash
curl -X POST "$LA_BASE/api/beta/assays" \
     -H "Authorization: ApiKey $LA_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "Plate 1",
       "description": "MTT readout.",
       "studyId": "f4a01b78-...",
       "assayTypeId": "8b2c3d4e-...",
       "status": "ACTIVE",
       "startDate": "2026-05-12"
     }'
```

**Choosing an ELN notebook (optional)**

If the assay's project is linked to an ELN, you can provision notebook content for the assay at creation time. Like a study, an assay inherits the project's ELN integration and gets its own child content. Omit both fields to leave ELN provisioning off.

Benchling — `benchling` object: creates a notebook folder under the parent study's Benchling folder and a notebook entry in it.

| Field            | Type   | Description                                                                |
| ---------------- | ------ | -------------------------------------------------------------------------- |
| `templateId`     | string | Optional Benchling entry template to apply. Omit for a blank entry.        |
| `templateFields` | object | Optional field values for the chosen template (`{ "Field name": value }`). |

CDD Vault — `useCddVault` boolean: when `true`, creates a notebook entry under the project's CDD Vault container. CDD Vault has no nested folders, so there is nothing else to configure per assay.

```bash
curl -X POST "$LA_BASE/api/beta/assays" \
     -H "Authorization: ApiKey $LA_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "Plate 1",
       "description": "MTT readout.",
       "studyId": "f4a01b78-...",
       "assayTypeId": "8b2c3d4e-...",
       "startDate": "2026-05-12",
       "benchling": {
         "templateId": "tmpl_abc123",
         "templateFields": { "Owner": "Jane Doe" }
       },
       "useCddVault": false
     }'
```

***

#### Notes

Notes are free-text annotations attached to a project, study, assay, or your organization. The v1 API exposes the current content of a note; full version history is internal-only. **Note endpoints are read-only on the public API** — create, update, and archive are performed through the Lab Atlas web UI.

| Method | Path                               | Description                       |
| ------ | ---------------------------------- | --------------------------------- |
| GET    | `/projects/{projectId}/notes`      | List notes on a project.          |
| GET    | `/projects/{projectId}/notes/{id}` | Fetch a single note.              |
| GET    | `/studies/{studyId}/notes`         | List notes on a study.            |
| GET    | `/studies/{studyId}/notes/{id}`    | Fetch a single note.              |
| GET    | `/assays/{assayId}/notes`          | List notes on an assay.           |
| GET    | `/assays/{assayId}/notes/{id}`     | Fetch a single note.              |
| GET    | `/organization-notes`              | List notes on your organization.  |
| GET    | `/organization-notes/{id}`         | Fetch a single organization note. |

Archived notes are not returned by either the list or get-by-id endpoints. The id returned by a list is the note's id and resolves directly via the matching get-by-id endpoint.

**Note representation**

A note's `type` is one of `NOTE`, `COMMENT`, `CONCLUSION`, `TEMPLATE`; its `format` is one of `TIPTAP`, `MARKDOWN`, `TEXT`, `QUILL`. The `content` field holds the current version's body.

***

#### Tasks

Tasks are checklist items attached to a study or assay.

| Method | Path                                         | Description                                              |
| ------ | -------------------------------------------- | -------------------------------------------------------- |
| GET    | `/studies/{studyId}/tasks`                   | List tasks.                                              |
| GET    | `/studies/{studyId}/tasks/{taskId}`          | Fetch a single task.                                     |
| POST   | `/studies/{studyId}/tasks`                   | Add a task.                                              |
| PUT    | `/studies/{studyId}/tasks/{taskId}`          | Update a task.                                           |
| PATCH  | `/studies/{studyId}/tasks/{taskId}/complete` | Mark a task complete. Optional body for completion data. |
| DELETE | `/studies/{studyId}/tasks/{taskId}`          | Remove a task.                                           |
| GET    | `/assays/{assayId}/tasks`                    | List tasks.                                              |
| GET    | `/assays/{assayId}/tasks/{taskId}`           | Fetch a single task.                                     |
| POST   | `/assays/{assayId}/tasks`                    | Add a task.                                              |
| PUT    | `/assays/{assayId}/tasks/{taskId}`           | Update.                                                  |
| PATCH  | `/assays/{assayId}/tasks/{taskId}/complete`  | Mark complete.                                           |
| DELETE | `/assays/{assayId}/tasks/{taskId}`           | Remove.                                                  |

**Task input body**

```json
{
  "label": "QC the raw data",
  "status": "TODO",
  "dueDate": "2026-05-20",
  "assignedToId": "73c1d4f2-..."
}
```

Status values: `TODO`, `COMPLETE`, `INCOMPLETE`.

***

#### External links

External links are URL references attached to a project, study, or assay.

| Method | Path                                            | Description          |
| ------ | ----------------------------------------------- | -------------------- |
| GET    | `/projects/{projectId}/external-links`          | List links.          |
| GET    | `/projects/{projectId}/external-links/{linkId}` | Fetch a single link. |
| POST   | `/projects/{projectId}/external-links`          | Add a link.          |
| PUT    | `/projects/{projectId}/external-links/{linkId}` | Update.              |
| DELETE | `/projects/{projectId}/external-links/{linkId}` | Remove.              |
| GET    | `/studies/{studyId}/external-links`             | List links.          |
| GET    | `/studies/{studyId}/external-links/{linkId}`    | Fetch.               |
| POST   | `/studies/{studyId}/external-links`             | Add.                 |
| PUT    | `/studies/{studyId}/external-links/{linkId}`    | Update.              |
| DELETE | `/studies/{studyId}/external-links/{linkId}`    | Remove.              |
| GET    | `/assays/{assayId}/external-links`              | List links.          |
| GET    | `/assays/{assayId}/external-links/{linkId}`     | Fetch.               |
| POST   | `/assays/{assayId}/external-links`              | Add.                 |
| PUT    | `/assays/{assayId}/external-links/{linkId}`     | Update.              |
| DELETE | `/assays/{assayId}/external-links/{linkId}`     | Remove.              |

**External-link input body**

```json
{
  "label": "Vendor data sheet",
  "url": "https://supplier.example.com/cat/AB12345.pdf"
}
```

***

#### Collaborators

Collaborators model the external organizations (CROs, partner labs, vendors) you work with. They are organization-scoped — every collaborator belongs to one Lab Atlas organization.

| Method | Path                  | Description                  |
| ------ | --------------------- | ---------------------------- |
| GET    | `/collaborators`      | Paginated list.              |
| GET    | `/collaborators/{id}` | Fetch a single collaborator. |
| POST   | `/collaborators`      | Create.                      |
| PUT    | `/collaborators/{id}` | Update.                      |
| DELETE | `/collaborators/{id}` | Remove.                      |

**Collaborator input body**

```json
{
  "label": "Acme CRO",
  "organizationName": "Acme Contract Research",
  "organizationLocation": "Boston, MA",
  "contactPersonName": "Jane Doe",
  "contactEmail": "jane.doe@acme.example.com",
  "code": "ACME",
  "active": true
}
```

***

#### Teams

Teams are reusable groups of organization users that can be granted access to projects in one step. **Team writes require `ORGANIZATION_ADMIN`.**

| Method | Path                | Description                                         | Auth   |
| ------ | ------------------- | --------------------------------------------------- | ------ |
| GET    | `/teams`            | Paginated list.                                     | Member |
| GET    | `/teams/{id}`       | Fetch a team.                                       | Member |
| POST   | `/teams`            | Create a team.                                      | Admin  |
| PUT    | `/teams/{id}`       | Update name, description, active flag, member list. | Admin  |
| POST   | `/teams/{id}/users` | Add users. Body: `{"userIds": ["...", "..."]}`.     | Admin  |
| DELETE | `/teams/{id}/users` | Remove users. Body: `{"userIds": [...]}`.           | Admin  |

***

#### Keywords

Keywords are simple tag strings shared across an organization.

| Method | Path             | Description                        |
| ------ | ---------------- | ---------------------------------- |
| GET    | `/keywords`      | Paginated list.                    |
| GET    | `/keywords/{id}` | Fetch a keyword.                   |
| POST   | `/keywords`      | Create. Body: `{"value": "AKT1"}`. |

Keyword values are case-sensitive and must be unique within an organization.

***

#### Assay types

Assay types define the metadata schema (fields and standard tasks) for assays. **Assay-type writes require `ORGANIZATION_ADMIN`.**

| Method | Path                       | Description                                                | Auth   |
| ------ | -------------------------- | ---------------------------------------------------------- | ------ |
| GET    | `/assay-types`             | Paginated list.                                            | Member |
| GET    | `/assay-types/{id}`        | Fetch a single assay type, including its fields and tasks. | Member |
| POST   | `/assay-types`             | Create.                                                    | Admin  |
| PUT    | `/assay-types/{id}`        | Update.                                                    | Admin  |
| POST   | `/assay-types/{id}/status` | Change status. Body: `{"status": "ACTIVE"}`.               | Admin  |
| DELETE | `/assay-types/{id}`        | Delete.                                                    | Admin  |

Status values: `DRAFT`, `ACTIVE`, `INACTIVE`, `ARCHIVED`, `DELETED`.

***

#### Study collections

Study collections are named groupings of studies, useful for ad-hoc reporting or cross-project comparisons.

| Method | Path                                        | Description                                       |
| ------ | ------------------------------------------- | ------------------------------------------------- |
| GET    | `/study-collections`                        | List collections visible to the caller.           |
| GET    | `/study-collections/{id}`                   | Fetch a single collection (includes its studies). |
| POST   | `/study-collections`                        | Create.                                           |
| PUT    | `/study-collections/{id}`                   | Update name/description/visibility.               |
| DELETE | `/study-collections/{id}`                   | Delete.                                           |
| POST   | `/study-collections/{id}/studies/{studyId}` | Add a study to the collection.                    |
| DELETE | `/study-collections/{id}/studies/{studyId}` | Remove a study from the collection.               |

**Create body**

```json
{
  "name": "Q2 hit follow-up",
  "description": "Studies tracking validated hits from Q1.",
  "visibility": "PROTECTED"
}
```

***

#### Study relationships

Study relationships record links between studies — e.g. "is parent of", "is blocked by", "is related to".

| Method | Path                                                | Description                                          |
| ------ | --------------------------------------------------- | ---------------------------------------------------- |
| GET    | `/studies/{studyId}/relationships`                  | List relationships originating from the given study. |
| POST   | `/studies/{studyId}/relationships`                  | Create a new relationship.                           |
| DELETE | `/studies/{studyId}/relationships/{relationshipId}` | Remove a relationship.                               |

**Create body**

```json
{
  "type": "IS_RELATED_TO",
  "targetStudyId": "73c1d4f2-..."
}
```

Relationship types: `IS_RELATED_TO`, `IS_PARENT_OF`, `IS_CHILD_OF`, `IS_BLOCKING`, `IS_BLOCKED_BY`, `IS_PRECEDED_BY`, `IS_SUCCEEDED_BY`.

When you create a relationship, the inverse relationship is automatically created on the target study.

***

#### Activity

Activity records describe events that have happened in the organization — study status changes, assay creation, note edits, and so on. Activity is **read-only**; events are generated by Lab Atlas itself as side effects of other API calls.

Visibility follows the same rules as the underlying resources: a caller can only see activity for projects, studies, and assays they are allowed to view. Cross-organization lookups return `404`.

| Method | Path                                | Description                                         |
| ------ | ----------------------------------- | --------------------------------------------------- |
| GET    | `/activity`                         | Paginated org-wide activity visible to the caller.  |
| GET    | `/activity/{id}`                    | Fetch a single activity event by UUID.              |
| GET    | `/projects/{id}/activity`           | Activity scoped to a single project.                |
| GET    | `/studies/{id}/activity`            | Activity scoped to a single study.                  |
| GET    | `/assays/{id}/activity`             | Activity scoped to a single assay.                  |
| GET    | `/organization-users/{id}/activity` | Activity performed by a specific organization user. |

**Activity DTO shape**

```json
{
  "id": "1f3e6c14-...",
  "eventType": "study.status_changed",
  "data": { "oldStatus": "ACTIVE", "newStatus": "COMPLETE" },
  "date": "2026-05-06T14:32:11.000+00:00",
  "organizationId": "b1d1b15a-...",
  "projectId": "8b2c3d4e-...",
  "studyId": "f4a01b78-...",
  "assayId": null,
  "organizationUserId": "73c1d4f2-...",
  "user": { "id": "73c1d4f2-...", "organizationId": "b1d1b15a-...", "user": { /* UserDtoV1 */ }, "roles": ["ORGANIZATION_USER"], "status": "ACTIVE" }
}
```

The `eventType` field is a dotted string (e.g. `study.created`, `assay.status_changed`, `note.updated`). The shape of `data` depends on the event type — treat it as an opaque map of strings to JSON values when consuming generically.

***

#### Storage folders

Storage folders link a project, study, or assay to a folder on a connected storage drive. Each record can have several attached folders, one of which is the **primary** folder. `{folderId}` is the id of the attachment (the join record), not the underlying drive folder. Responses contain folder metadata only — the API never lists the folder's subfolders or files.

| Method | Path                                               | Description                                          |
| ------ | -------------------------------------------------- | ---------------------------------------------------- |
| GET    | `/projects/{projectId}/storage-folders`            | List folders attached to a project.                  |
| GET    | `/projects/{projectId}/storage-folders/{folderId}` | Fetch a single attachment.                           |
| POST   | `/projects/{projectId}/storage-folders`            | Attach an existing drive folder. Returns `201`.      |
| PATCH  | `/projects/{projectId}/storage-folders/{folderId}` | Update the attachment (set as primary).              |
| DELETE | `/projects/{projectId}/storage-folders/{folderId}` | Detach the folder (the drive folder is left intact). |
| GET    | `/studies/{studyId}/storage-folders`               | List folders attached to a study.                    |
| GET    | `/studies/{studyId}/storage-folders/{folderId}`    | Fetch a single attachment.                           |
| POST   | `/studies/{studyId}/storage-folders`               | Attach an existing drive folder.                     |
| PATCH  | `/studies/{studyId}/storage-folders/{folderId}`    | Set as primary.                                      |
| DELETE | `/studies/{studyId}/storage-folders/{folderId}`    | Detach.                                              |
| GET    | `/assays/{assayId}/storage-folders`                | List folders attached to an assay.                   |
| GET    | `/assays/{assayId}/storage-folders/{folderId}`     | Fetch a single attachment.                           |
| POST   | `/assays/{assayId}/storage-folders`                | Attach an existing drive folder.                     |
| PATCH  | `/assays/{assayId}/storage-folders/{folderId}`     | Set as primary.                                      |
| DELETE | `/assays/{assayId}/storage-folders/{folderId}`     | Detach.                                              |

All write operations require **contribute** permission on the parent project/study/assay.

**Attach body**

```json
{
  "driveId": "8b2c3d4e-...",
  "path": "/bucket/projects/CPA/Data"
}
```

The folder at `path` must already exist on the drive. **Set-primary body** is `{"primary": true}`.

***

#### Read-only resources

The following resources are exposed for **reads only** in the public API. Create/update/delete must be performed through the Lab Atlas web UI.

| Resource               | Endpoints                                                         |
| ---------------------- | ----------------------------------------------------------------- |
| Users                  | `GET /users`, `GET /users/{id}`                                   |
| Organization users     | `GET /organization-users`, `GET /organization-users/{id}`         |
| Integrations           | `GET /integrations`, `GET /integrations/{id}`                     |
| Storage drives         | `GET /storage-drives`, `GET /storage-drives/{id}`                 |
| Shared storage folders | `GET /shared-storage-folders`, `GET /shared-storage-folders/{id}` |

***

### Errors

Errors are returned as a JSON envelope:

```json
{
  "timestamp": "2026-05-06T14:32:11.000+00:00",
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found: b1d1b15a-19e0-4d6f-8d33-7a3d3aef6cba",
  "path": "/api/beta/projects/b1d1b15a-19e0-4d6f-8d33-7a3d3aef6cba"
}
```

For validation failures (`400`), additional fields are included describing each invalid field:

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Validation failed",
  "details": "code: must not be blank; description: must not be blank",
  "path": "/api/beta/projects",
  "errors": [
    { "field": "code",        "message": "must not be blank" },
    { "field": "description", "message": "must not be blank" }
  ]
}
```

The `statusCode` field carries the HTTP status as an integer (separate from the per-resource `status` enum some response bodies carry). The `details` field is a one-line human-readable summary of all field violations — handy for log lines and quick triage; for programmatic handling, iterate `errors[]`.

A request that authenticates successfully but is denied at the controller layer (e.g. an admin endpoint called by a non-admin key) returns `403 Forbidden` with an empty body. A request that targets a resource the caller cannot view returns `404 Not Found` rather than `403` — this avoids leaking the existence of records the caller has no business knowing about.

***

### OpenAPI / Swagger

The full machine-readable specification is available at:

* **Swagger UI**: `https://<your-tenant>.labatlas.com/docs/swagger-ui` (select the **public-v1** group)
* **OpenAPI JSON**: `https://<your-tenant>.labatlas.com/docs/api-docs/public-v1`

The Swagger UI is gated behind the same API-key check as the API itself — paste your key into the **Authorize** dialog (header name `X-API-KEY`) to try requests inline.

If you generate client SDKs from the OpenAPI document, regenerate after each Lab Atlas release; field additions are backwards-compatible but new endpoints may appear.

***

### Support

If you hit a bug, need an endpoint that isn't listed here, or want to raise a security concern, email **<support@labatlas.com>** and include:

* The full URL of the failing request
* The HTTP status code returned
* The response body (with any sensitive data redacted)
* A timestamp (UTC) so support can correlate against server logs
* The **key id** portion of the API key in use (the `lak_xxxxxxxx` prefix is safe to share; never share the secret after the dot)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.labatlas.com/rest-api-reference.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
