> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fitsociety.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Forms, checkups, intake, and documents

> Assign forms, read submissions, and list document metadata through the FITsociety Public API.

Forms and documents can contain health data and uploaded media. Public API v1
returns metadata and text answer DTOs only. Media answers are redacted and
document download URLs are not exposed.

## Scopes

| Scope                    | Allows                                                                  |
| :----------------------- | :---------------------------------------------------------------------- |
| `forms:read`             | Read form metadata.                                                     |
| `form_assignments:write` | Assign an intake form to a client.                                      |
| `intakes:read`           | Read current intake assignment status and latest submitted intake form. |
| `checkups:read`          | Read assigned checkups and submitted checkup answers.                   |
| `checkups:write`         | Assign or cancel a checkup schedule.                                    |
| `documents:read`         | Read document folder and document metadata.                             |
| `documents:write`        | Link, update, and archive external document metadata.                   |

## List forms

```http theme={null}
GET /public/v1/forms?formType=intake
Authorization: Bearer <access_token>
```

Required scope: `forms:read`

Query parameters:

| Parameter  | Type    | Required | Rule                          |
| :--------- | :------ | :------- | :---------------------------- |
| `formType` | string  | no       | `intake` or `checkup`.        |
| `page`     | integer | no       | Minimum `1`.                  |
| `limit`    | integer | no       | Default `100`, maximum `100`. |

Response fields:

| Field                          | Type    | Nullable | Description                                   |
| :----------------------------- | :------ | :------- | :-------------------------------------------- |
| `data.forms[].formId`          | string  | no       | Form ID.                                      |
| `data.forms[].name`            | string  | no       | Form name.                                    |
| `data.forms[].description`     | string  | no       | Form description.                             |
| `data.forms[].formType`        | string  | no       | `intake` or `checkup`.                        |
| `data.forms[].sectionCount`    | integer | no       | Number of sections.                           |
| `data.forms[].questionCount`   | integer | no       | Number of questions.                          |
| `data.forms[].hasObjectives`   | boolean | no       | Whether objectives are configured.            |
| `data.forms[].hasMeasurements` | boolean | no       | Whether measurement questions are configured. |
| `data.forms[].createdAt`       | string  | yes      | Creation timestamp.                           |
| `data.forms[].updatedAt`       | string  | yes      | Update timestamp.                             |

Form sections, question definitions, objective definitions, and media are not
included in this metadata endpoint.

<CodeGroup>
  ```bash cURL theme={null}
  curl -H "Authorization: Bearer $FITSOCIETY_ACCESS_TOKEN" \
    "https://api.fitsociety.io/public/v1/forms?formType=intake&page=1&limit=100"
  ```

  ```javascript JavaScript theme={null}
  const query = new URLSearchParams({
    formType: "intake",
    page: "1",
    limit: "100",
  });

  const response = await fetch(
    `https://api.fitsociety.io/public/v1/forms?${query}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.FITSOCIETY_ACCESS_TOKEN}`,
      },
    },
  );

  const body = await response.json();
  console.log(body.data.total, body.data.forms);
  ```

  ```python Python theme={null}
  import os
  import requests

  response = requests.get(
      "https://api.fitsociety.io/public/v1/forms",
      headers={"Authorization": f"Bearer {os.environ['FITSOCIETY_ACCESS_TOKEN']}"},
      params={"formType": "intake", "page": 1, "limit": 100},
  )
  response.raise_for_status()

  body = response.json()
  print(body["data"]["total"], body["data"]["forms"])
  ```
</CodeGroup>

Response:

```json theme={null}
{
  "data": {
    "page": 1,
    "limit": 100,
    "total": 1,
    "totalPages": 1,
    "hasNextPage": false,
    "hasPrevPage": false,
    "forms": [
      {
        "formId": "64b64c0f2f5f4c00123456d0",
        "name": "New client intake",
        "description": "Intake questionnaire for new clients",
        "formType": "intake",
        "sectionCount": 3,
        "questionCount": 18,
        "hasObjectives": true,
        "hasMeasurements": false,
        "createdAt": "2026-05-01T09:00:00.000Z",
        "updatedAt": "2026-07-01T09:00:00.000Z"
      }
    ]
  },
  "meta": {
    "requestId": "req_0123456789abcdef",
    "rateLimit": { "limit": 10, "remaining": 9, "resetSeconds": 1 }
  }
}
```

## Get form detail

```http theme={null}
GET /public/v1/forms/{formId}
Authorization: Bearer <access_token>
```

Required scope: `forms:read`

This endpoint returns a sanitized form structure for integrations that need to
render supported questions. It does not return objectives, medical/nutrition
blocks, measurement configuration, form media, AI metadata, coach IDs, or raw
model fields.

Response fields:

| Field                                                     | Type      | Nullable | Description                             |
| :-------------------------------------------------------- | :-------- | :------- | :-------------------------------------- |
| `data.form.formId`                                        | string    | no       | Form ID.                                |
| `data.form.name`                                          | string    | no       | Form name.                              |
| `data.form.description`                                   | string    | no       | Form description.                       |
| `data.form.formType`                                      | string    | no       | `intake` or `checkup`.                  |
| `data.form.sectionCount`                                  | integer   | no       | Number of sections.                     |
| `data.form.questionCount`                                 | integer   | no       | Number of questions.                    |
| `data.form.hasObjectives`                                 | boolean   | no       | Whether hidden objective blocks exist.  |
| `data.form.hasMeasurements`                               | boolean   | no       | Whether measurement blocks exist.       |
| `data.form.sections[].sectionId`                          | string    | no       | Section ID.                             |
| `data.form.sections[].title`                              | string    | no       | Section title.                          |
| `data.form.sections[].description`                        | string    | no       | Section description.                    |
| `data.form.sections[].questions[].questionId`             | string    | no       | Question ID.                            |
| `data.form.sections[].questions[].name`                   | string    | no       | Question label.                         |
| `data.form.sections[].questions[].type`                   | string    | no       | Question type.                          |
| `data.form.sections[].questions[].required`               | boolean   | no       | Required flag.                          |
| `data.form.sections[].questions[].options[]`              | string\[] | no       | Options for choice/checklist questions. |
| `data.form.sections[].questions[].linearScale`            | object    | yes      | Linear scale metadata when configured.  |
| `data.form.sections[].questions[].linearScale.startValue` | number    | yes      | Linear scale start value.               |
| `data.form.sections[].questions[].linearScale.endValue`   | number    | yes      | Linear scale end value.                 |
| `data.form.sections[].questions[].linearScale.startLabel` | string    | no       | Linear scale start label.               |
| `data.form.sections[].questions[].linearScale.endLabel`   | string    | no       | Linear scale end label.                 |
| `data.form.sections[].questions[].ratingScale`            | number    | yes      | Rating scale maximum when configured.   |
| `data.form.sections[].questions[].maxFiles`               | number    | yes      | Media count limit for media questions.  |
| `data.form.sections[].questions[].order`                  | number    | yes      | Display order.                          |

## Assign intake form

```http theme={null}
POST /public/v1/clients/{clientId}/intake-assignments
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `form_assignments:write`

Request body:

| Field    | Type   | Required | Rule                                                             |
| :------- | :----- | :------- | :--------------------------------------------------------------- |
| `formId` | string | yes      | Existing non-deleted `intake` form in the authenticated company. |

Assigning an intake form makes it the current form for the client and marks
previous current intake assignments as not current.

Response fields:

| Field                             | Type    | Nullable | Description                              |
| :-------------------------------- | :------ | :------- | :--------------------------------------- |
| `data.assignment.assignmentId`    | string  | no       | Assignment ID.                           |
| `data.assignment.clientId`        | string  | no       | Client ID.                               |
| `data.assignment.formId`          | string  | no       | Form ID.                                 |
| `data.assignment.formName`        | string  | no       | Form name.                               |
| `data.assignment.status`          | string  | no       | Assignment status.                       |
| `data.assignment.isAccessEnabled` | boolean | no       | Whether client access is enabled.        |
| `data.assignment.isCurrentForm`   | boolean | no       | Whether this is the current intake form. |
| `data.assignment.assignedAt`      | string  | yes      | Assignment creation timestamp.           |
| `data.assignment.updatedAt`       | string  | yes      | Assignment update timestamp.             |

## Get current intake assignment

```http theme={null}
GET /public/v1/clients/{clientId}/intake-assignment
Authorization: Bearer <access_token>
```

Required scope: `intakes:read`

Response fields:

| Field                             | Type    | Nullable | Description                                                      |
| :-------------------------------- | :------ | :------- | :--------------------------------------------------------------- |
| `data.status`                     | string  | no       | `not_assigned`, `active`, `completed`, `disabled`, or `expired`. |
| `data.assignment`                 | object  | yes      | Current assignment metadata, or `null`.                          |
| `data.assignment.assignmentId`    | string  | no       | Assignment ID.                                                   |
| `data.assignment.clientId`        | string  | no       | Client ID.                                                       |
| `data.assignment.formId`          | string  | no       | Form ID.                                                         |
| `data.assignment.formName`        | string  | no       | Form name.                                                       |
| `data.assignment.status`          | string  | no       | Assignment status.                                               |
| `data.assignment.isAccessEnabled` | boolean | no       | Whether client access is enabled.                                |
| `data.assignment.isCurrentForm`   | boolean | no       | Whether this is the current intake form.                         |
| `data.assignment.assignedAt`      | string  | yes      | Assignment creation timestamp.                                   |
| `data.assignment.updatedAt`       | string  | yes      | Assignment update timestamp.                                     |

Not exposed: coach IDs, raw form structure, form media, disabled assignment
history, or draft answers.

## Get latest intake submission

```http theme={null}
GET /public/v1/clients/{clientId}/intake/submission
Authorization: Bearer <access_token>
```

Required scope: `intakes:read`

Response fields:

| Field                              | Type    | Nullable | Description                                              |
| :--------------------------------- | :------ | :------- | :------------------------------------------------------- |
| `data.intake.submissionId`         | string  | no       | Submission ID.                                           |
| `data.intake.formId`               | string  | yes      | Form ID.                                                 |
| `data.intake.formName`             | string  | no       | Form name.                                               |
| `data.intake.formType`             | string  | no       | `intake`.                                                |
| `data.intake.settingId`            | string  | yes      | Setting ID when applicable.                              |
| `data.intake.submittedFor`         | string  | yes      | Submission period/date.                                  |
| `data.intake.submittedAt`          | string  | yes      | Submission timestamp.                                    |
| `data.intake.isReviewed`           | boolean | no       | Review status.                                           |
| `data.intake.answers[].question`   | string  | no       | Question name.                                           |
| `data.intake.answers[].answerType` | string  | no       | Answer type.                                             |
| `data.intake.answers[].answer`     | string  | no       | Normalized answer. Media answers are `[media redacted]`. |
| `data.intake.answers[].answeredAt` | string  | yes      | Answer timestamp.                                        |
| `data.intake.feedbacks[].feedback` | string  | no       | Feedback text.                                           |
| `data.intake.feedbacks[].givenAt`  | string  | yes      | Feedback timestamp.                                      |

## List assigned checkups

```http theme={null}
GET /public/v1/clients/{clientId}/checkups
Authorization: Bearer <access_token>
```

Required scope: `checkups:read`

Response fields:

| Field                                  | Type   | Nullable | Description             |
| :------------------------------------- | :----- | :------- | :---------------------- |
| `data.checkups[].settingId`            | string | no       | Checkup setting ID.     |
| `data.checkups[].formId`               | string | yes      | Form ID.                |
| `data.checkups[].formName`             | string | no       | Form name.              |
| `data.checkups[].scheduleType`         | string | no       | Schedule type.          |
| `data.checkups[].lastAnswerDate`       | string | yes      | Last answer date.       |
| `data.checkups[].nextNotificationDate` | string | yes      | Next notification date. |

## Get checkup status

```http theme={null}
GET /public/v1/clients/{clientId}/checkups/{settingId}/status
Authorization: Bearer <access_token>
```

Required scope: `checkups:read`

Response fields:

| Field                             | Type    | Nullable | Description                                         |
| :-------------------------------- | :------ | :------- | :-------------------------------------------------- |
| `data.checkup.settingId`          | string  | no       | Checkup setting ID.                                 |
| `data.checkup.clientId`           | string  | no       | Client ID.                                          |
| `data.checkup.formId`             | string  | no       | Form ID.                                            |
| `data.checkup.formName`           | string  | no       | Form name.                                          |
| `data.checkup.status`             | string  | no       | `pending`, `submitted`, `disabled`, or `cancelled`. |
| `data.checkup.isActive`           | boolean | no       | Whether the assignment is active.                   |
| `data.checkup.scheduleType`       | string  | no       | Schedule type only.                                 |
| `data.checkup.lastSubmittedAt`    | string  | yes      | Last submitted timestamp.                           |
| `data.checkup.nextNotificationAt` | string  | yes      | Next notification timestamp.                        |
| `data.checkup.createdAt`          | string  | yes      | Creation timestamp.                                 |
| `data.checkup.updatedAt`          | string  | yes      | Last update timestamp.                              |

Not exposed: reminder email/chat bodies, raw schedule internals, coach IDs,
deleted flags, or draft answers.

## List checkup submissions

```http theme={null}
GET /public/v1/clients/{clientId}/checkups/submissions?dateFrom=2026-01-01&dateTo=2026-07-13
Authorization: Bearer <access_token>
```

Required scope: `checkups:read`

Query parameters:

| Parameter   | Type           | Required | Rule                          |
| :---------- | :------------- | :------- | :---------------------------- |
| `page`      | integer        | no       | Minimum `1`.                  |
| `limit`     | integer        | no       | Default `25`, maximum `25`.   |
| `settingId` | string         | no       | Filter by checkup setting.    |
| `dateFrom`  | date/date-time | no       | Must be paired with `dateTo`. |
| `dateTo`    | date/date-time | no       | Max 366-day range.            |

Response fields:

| Field                                     | Type    | Nullable | Description                                              |
| :---------------------------------------- | :------ | :------- | :------------------------------------------------------- |
| `data.page`                               | integer | no       | Current page.                                            |
| `data.limit`                              | integer | no       | Page size after cap, maximum `25`.                       |
| `data.total`                              | integer | no       | Total matching submissions.                              |
| `data.totalPages`                         | integer | no       | Total pages.                                             |
| `data.hasNextPage`                        | boolean | no       | Next page availability.                                  |
| `data.hasPrevPage`                        | boolean | no       | Previous page availability.                              |
| `data.submissions[].submissionId`         | string  | no       | Submission ID.                                           |
| `data.submissions[].formId`               | string  | yes      | Form ID.                                                 |
| `data.submissions[].formName`             | string  | no       | Form name.                                               |
| `data.submissions[].formType`             | string  | no       | `checkup`.                                               |
| `data.submissions[].settingId`            | string  | yes      | Checkup setting ID when applicable.                      |
| `data.submissions[].submittedFor`         | string  | yes      | Submission period/date.                                  |
| `data.submissions[].submittedAt`          | string  | yes      | Submission timestamp.                                    |
| `data.submissions[].isReviewed`           | boolean | no       | Review status.                                           |
| `data.submissions[].answers[].question`   | string  | no       | Question name.                                           |
| `data.submissions[].answers[].answerType` | string  | no       | Answer type.                                             |
| `data.submissions[].answers[].answer`     | string  | no       | Normalized answer. Media answers are `[media redacted]`. |
| `data.submissions[].answers[].answeredAt` | string  | yes      | Answer timestamp.                                        |
| `data.submissions[].feedbacks[].feedback` | string  | no       | Feedback text.                                           |
| `data.submissions[].feedbacks[].givenAt`  | string  | yes      | Feedback timestamp.                                      |

## Assign checkup

```http theme={null}
POST /public/v1/clients/{clientId}/checkups
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `checkups:write`

Request body:

| Field                               | Type    | Required    | Rule                                                                        |
| :---------------------------------- | :------ | :---------- | :-------------------------------------------------------------------------- |
| `formId`                            | string  | yes         | Existing non-deleted `checkup` form in the authenticated company.           |
| `schedule.type`                     | string  | yes         | `Today`, `Daily`, `Weekly`, `Monthly`, `Weekends`, `Weekdays`, or `Custom`. |
| `schedule.weeklyOn`                 | string  | conditional | Required for `Weekly`; weekday name.                                        |
| `schedule.monthlyWeek`              | string  | conditional | Required for `Monthly`; `First`, `Second`, `Third`, `Fourth`, or `Last`.    |
| `schedule.monthlyDay`               | string  | conditional | Required for `Monthly`; weekday name.                                       |
| `schedule.custom.startDate`         | date    | conditional | Required for `Custom`.                                                      |
| `schedule.custom.repeatEvery.count` | integer | no          | Must be positive when supplied.                                             |
| `schedule.custom.repeatEvery.unit`  | string  | no          | `day`, `week`, `month`, or `year`.                                          |

Public API v1 does not accept reminder email/chat bodies. Reminder mutation,
feedback writes, review toggles, and schedule delete/toggle remain internal.

Response fields:

| Field                               | Type   | Nullable | Description                 |
| :---------------------------------- | :----- | :------- | :-------------------------- |
| `data.checkup.settingId`            | string | no       | Created checkup setting ID. |
| `data.checkup.formId`               | string | yes      | Form ID.                    |
| `data.checkup.formName`             | string | no       | Form name.                  |
| `data.checkup.scheduleType`         | string | no       | Schedule type.              |
| `data.checkup.lastAnswerDate`       | string | yes      | Last answer date.           |
| `data.checkup.nextNotificationDate` | string | yes      | Next notification date.     |

## Cancel checkup

```http theme={null}
POST /public/v1/clients/{clientId}/checkups/{settingId}/cancel
Authorization: Bearer <access_token>
```

Required scope: `checkups:write`

This is a soft cancel. It marks the checkup setting as deleted and clears
`nextNotificationDate`; it does not delete submitted answers.

Validation:

| Parameter   | Type   | Required | Rule                                                |
| :---------- | :----- | :------- | :-------------------------------------------------- |
| `clientId`  | string | yes      | Client must belong to the authenticated company.    |
| `settingId` | string | yes      | Active checkup setting for that client and company. |

Response fields:

| Field                             | Type    | Nullable | Description                            |
| :-------------------------------- | :------ | :------- | :------------------------------------- |
| `data.checkup.settingId`          | string  | no       | Checkup setting ID.                    |
| `data.checkup.clientId`           | string  | no       | Client ID.                             |
| `data.checkup.formId`             | string  | no       | Form ID.                               |
| `data.checkup.formName`           | string  | no       | Form name.                             |
| `data.checkup.status`             | string  | no       | `cancelled` after a successful cancel. |
| `data.checkup.isActive`           | boolean | no       | `false` after a successful cancel.     |
| `data.checkup.scheduleType`       | string  | no       | Schedule type only.                    |
| `data.checkup.lastSubmittedAt`    | string  | yes      | Last submitted timestamp.              |
| `data.checkup.nextNotificationAt` | string  | yes      | `null` after cancellation.             |
| `data.checkup.createdAt`          | string  | yes      | Creation timestamp.                    |
| `data.checkup.updatedAt`          | string  | yes      | Last update timestamp.                 |

## List document folders

```http theme={null}
GET /public/v1/clients/{clientId}/document-folders
Authorization: Bearer <access_token>
```

Required scope: `documents:read`

Response fields:

| Field                                 | Type    | Nullable | Description                 |
| :------------------------------------ | :------ | :------- | :-------------------------- |
| `data.page`                           | integer | no       | Current page.               |
| `data.limit`                          | integer | no       | Page size after cap.        |
| `data.total`                          | integer | no       | Total matching folders.     |
| `data.totalPages`                     | integer | no       | Total pages.                |
| `data.hasNextPage`                    | boolean | no       | Next page availability.     |
| `data.hasPrevPage`                    | boolean | no       | Previous page availability. |
| `data.folders[].folderId`             | string  | no       | Folder ID.                  |
| `data.folders[].name`                 | string  | no       | Folder name.                |
| `data.folders[].clientId`             | string  | yes      | Client ID.                  |
| `data.folders[].sharedWithAllClients` | boolean | no       | Shared-default flag.        |
| `data.folders[].createdAt`            | string  | yes      | Creation timestamp.         |

## List documents

```http theme={null}
GET /public/v1/clients/{clientId}/documents?folderId={folderId}
Authorization: Bearer <access_token>
```

Required scope: `documents:read`

Validation:

| Parameter  | Type    | Required | Rule                          |
| :--------- | :------ | :------- | :---------------------------- |
| `page`     | integer | no       | Minimum `1`.                  |
| `limit`    | integer | no       | Default `100`, maximum `100`. |
| `folderId` | string  | no       | Valid folder ObjectId.        |

Response fields:

| Field                                   | Type    | Nullable | Description                 |
| :-------------------------------------- | :------ | :------- | :-------------------------- |
| `data.page`                             | integer | no       | Current page.               |
| `data.limit`                            | integer | no       | Page size after cap.        |
| `data.total`                            | integer | no       | Total matching documents.   |
| `data.totalPages`                       | integer | no       | Total pages.                |
| `data.hasNextPage`                      | boolean | no       | Next page availability.     |
| `data.hasPrevPage`                      | boolean | no       | Previous page availability. |
| `data.documents[].documentId`           | string  | no       | Document ID.                |
| `data.documents[].name`                 | string  | no       | Document name.              |
| `data.documents[].mimeType`             | string  | no       | MIME type.                  |
| `data.documents[].fileSize`             | string  | no       | File size as stored.        |
| `data.documents[].folderId`             | string  | yes      | Folder ID.                  |
| `data.documents[].clientId`             | string  | yes      | Client ID.                  |
| `data.documents[].sharedWithAllClients` | boolean | no       | Shared-default flag.        |
| `data.documents[].addedByRole`          | string  | no       | Uploader role.              |
| `data.documents[].createdAt`            | string  | yes      | Creation timestamp.         |

Not exposed in v1: upload, delete, folder mutation, signed document download
URLs, raw `media`, provider metadata, message IDs, file contents, objective
medical/nutrition blocks, draft form answers, reminder message bodies, or form
feedback mutation.

## Link external document metadata

```http theme={null}
POST /public/v1/clients/{clientId}/documents/links
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `documents:write`

This endpoint stores metadata for a document managed outside FITsociety. It does
not upload binary files and does not expose raw download links in list/read
responses.

Request body:

| Field                          | Type            | Required | Rule                                                                                |
| :----------------------------- | :-------------- | :------- | :---------------------------------------------------------------------------------- |
| `name`                         | string          | yes      | Trimmed, maximum 160 characters.                                                    |
| `documentUrl` or `externalUrl` | URL string      | yes      | Must be an absolute `http` or `https` URL.                                          |
| `mimeType` or `type`           | string          | no       | Trimmed, maximum 120 characters. Defaults to `application/octet-stream`.            |
| `fileSize`                     | string          | no       | Trimmed, maximum 40 characters.                                                     |
| `folderId`                     | ObjectId string | no       | Folder must belong to the client, be company-shared, or be a shared default folder. |

Validation:

| Rule          | Behavior                                                                   |
| :------------ | :------------------------------------------------------------------------- |
| Client access | Client must belong to the authenticated company.                           |
| Coach context | The OAuth client must have a usable default coach context for attribution. |
| Folder access | Invalid or inaccessible folders return a document-folder error.            |
| URL protocol  | Non-HTTP(S) URLs are rejected.                                             |

Response fields:

| Field                                | Type    | Nullable | Description          |
| :----------------------------------- | :------ | :------- | :------------------- |
| `data.document.documentId`           | string  | no       | Linked document ID.  |
| `data.document.name`                 | string  | no       | Document name.       |
| `data.document.mimeType`             | string  | no       | MIME type.           |
| `data.document.fileSize`             | string  | no       | File size as stored. |
| `data.document.folderId`             | string  | yes      | Folder ID.           |
| `data.document.clientId`             | string  | yes      | Client ID.           |
| `data.document.sharedWithAllClients` | boolean | no       | Shared-default flag. |
| `data.document.addedByRole`          | string  | no       | Uploader role.       |
| `data.document.createdAt`            | string  | yes      | Creation timestamp.  |

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.fitsociety.io/public/v1/clients/64b64c0f2f5f4c0012345678/documents/links" \
    -H "Authorization: Bearer $FITSOCIETY_ACCESS_TOKEN" \
    -H "Idempotency-Key: document-link-training-agreement-20260823" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Training agreement",
      "documentUrl": "https://files.example.com/agreements/jane-doe.pdf",
      "mimeType": "application/pdf",
      "fileSize": "182 KB",
      "folderId": "64b64c0f2f5f4c00123456e0"
    }'
  ```

  ```javascript JavaScript theme={null}
  const clientId = "64b64c0f2f5f4c0012345678";
  const response = await fetch(
    `https://api.fitsociety.io/public/v1/clients/${clientId}/documents/links`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.FITSOCIETY_ACCESS_TOKEN}`,
        "Idempotency-Key": "document-link-training-agreement-20260823",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: "Training agreement",
        documentUrl: "https://files.example.com/agreements/jane-doe.pdf",
        mimeType: "application/pdf",
        fileSize: "182 KB",
        folderId: "64b64c0f2f5f4c00123456e0",
      }),
    },
  );

  const body = await response.json();
  if (response.status === 201) {
    console.log("Linked document", body.data.document.documentId);
  }
  ```

  ```python Python theme={null}
  import os
  import requests

  client_id = "64b64c0f2f5f4c0012345678"
  response = requests.post(
      f"https://api.fitsociety.io/public/v1/clients/{client_id}/documents/links",
      headers={
          "Authorization": f"Bearer {os.environ['FITSOCIETY_ACCESS_TOKEN']}",
          "Idempotency-Key": "document-link-training-agreement-20260823",
      },
      json={
          "name": "Training agreement",
          "documentUrl": "https://files.example.com/agreements/jane-doe.pdf",
          "mimeType": "application/pdf",
          "fileSize": "182 KB",
          "folderId": "64b64c0f2f5f4c00123456e0",
      },
  )
  response.raise_for_status()

  body = response.json()
  if response.status_code == 201:
      print("Linked document", body["data"]["document"]["documentId"])
  ```
</CodeGroup>

Response (`201 Created`):

```json theme={null}
{
  "data": {
    "document": {
      "documentId": "64b64c0f2f5f4c00123456e1",
      "name": "Training agreement",
      "mimeType": "application/pdf",
      "fileSize": "182 KB",
      "folderId": "64b64c0f2f5f4c00123456e0",
      "clientId": "64b64c0f2f5f4c0012345678",
      "sharedWithAllClients": false,
      "addedByRole": "coach",
      "createdAt": "2026-08-23T10:00:00.000Z"
    }
  },
  "meta": {
    "requestId": "req_0123456789abcdef",
    "rateLimit": { "limit": 10, "remaining": 8, "resetSeconds": 1 }
  }
}
```

## Update document metadata

```http theme={null}
PATCH /public/v1/clients/{clientId}/documents/{documentId}
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `documents:write`

Allowed fields: `name`, `documentUrl`, `externalUrl`, `mimeType`, `type`,
`fileSize`, and `folderId`. At least one field is required. The same name, URL,
type, file-size, client, and folder validations as Link external document
metadata apply.

Response fields:

| Field                                | Type    | Nullable | Description          |
| :----------------------------------- | :------ | :------- | :------------------- |
| `data.document.documentId`           | string  | no       | Document ID.         |
| `data.document.name`                 | string  | no       | Document name.       |
| `data.document.mimeType`             | string  | no       | MIME type.           |
| `data.document.fileSize`             | string  | no       | File size as stored. |
| `data.document.folderId`             | string  | yes      | Folder ID.           |
| `data.document.clientId`             | string  | yes      | Client ID.           |
| `data.document.sharedWithAllClients` | boolean | no       | Shared-default flag. |
| `data.document.addedByRole`          | string  | no       | Uploader role.       |
| `data.document.createdAt`            | string  | yes      | Creation timestamp.  |

## Archive document metadata

```http theme={null}
PATCH /public/v1/clients/{clientId}/documents/{documentId}/archive
Authorization: Bearer <access_token>
```

Required scope: `documents:write`

Archive is a metadata-only soft archive. The document is excluded from Public
API list responses after archiving. It does not delete a binary file from
external storage.

Response fields:

| Field                      | Type    | Nullable | Description                             |
| :------------------------- | :------ | :------- | :-------------------------------------- |
| `data.document.documentId` | string  | no       | Document ID.                            |
| `data.document.archived`   | boolean | no       | Always `true` for successful responses. |
| `data.document.updatedAt`  | string  | yes      | Update timestamp.                       |
