> ## 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.

# Measurements and progress

> Read measurement types, manage measurement entries, and read progress metadata through the FITsociety Public API.

Public API v1 exposes measurement entries, soft-archive, progress summaries,
and read-only progress photos with short-lived signed URLs. Progress photo
upload/hard delete and objective writes are not exposed because they involve
media handling and broader health-data privacy surfaces.

## Scopes

| Scope                     | Allows                                                          |
| :------------------------ | :-------------------------------------------------------------- |
| `measurements:read`       | Read measurement types and client measurement entries.          |
| `measurements:write`      | Create, update, and soft-archive measurement entries.           |
| `progress_summaries:read` | Read aggregated measurement progress summaries.                 |
| `progress_photos:read`    | Read progress photo metadata and short-lived signed image URLs. |

<Note>
  All four scopes on this page are consent-gated health scopes: the API client
  must be created with `"consents": { "healthData": true }`. Without that
  stored consent, these endpoints return `403 scopes.health_consent_required`.
  See [Authentication](/public-api/authentication#consent-gated-scopes).
</Note>

## List measurement types

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

Required scope: `measurements:read`

Response fields:

| Field                         | Type      | Nullable | Description                                |
| :---------------------------- | :-------- | :------- | :----------------------------------------- |
| `data.types[].typeId`         | string    | no       | Measurement type ID.                       |
| `data.types[].label`          | string    | no       | Stable type label.                         |
| `data.types[].name`           | string    | no       | Localized type name.                       |
| `data.types[].scope`          | string    | no       | `system`, `company`, or `client`.          |
| `data.types[].baseUnit`       | string    | no       | Default unit. Empty string means unitless. |
| `data.types[].allowedUnits[]` | string\[] | no       | Allowed units for the type.                |
| `data.types[].color`          | string    | no       | Hex color configured for the type.         |

## List measurement entries

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

Required scope: `measurements:read`

Validation:

| Parameter  | Type           | Required | Rule                                               |
| :--------- | :------------- | :------- | :------------------------------------------------- |
| `page`     | integer        | no       | Minimum `1`.                                       |
| `limit`    | integer        | no       | Default `100`, maximum `100`.                      |
| `typeId`   | string         | no       | Valid measurement type ID.                         |
| `dateFrom` | date/date-time | no       | Must be paired with `dateTo`.                      |
| `dateTo`   | date/date-time | no       | Must be paired with `dateFrom`, max 366-day range. |

Response fields:

| Field                            | Type    | Nullable | Description                       |
| :------------------------------- | :------ | :------- | :-------------------------------- |
| `data.page`                      | integer | no       | Current page.                     |
| `data.limit`                     | integer | no       | Page size.                        |
| `data.total`                     | integer | no       | Total entries.                    |
| `data.totalPages`                | integer | no       | Total pages.                      |
| `data.hasNextPage`               | boolean | no       | Next page availability.           |
| `data.hasPrevPage`               | boolean | no       | Previous page availability.       |
| `data.measurements[].entryId`    | string  | no       | Entry ID.                         |
| `data.measurements[].typeId`     | string  | yes      | Measurement type ID.              |
| `data.measurements[].value`      | number  | yes      | Numeric value.                    |
| `data.measurements[].unit`       | string  | no       | Unit at entry time.               |
| `data.measurements[].measuredAt` | string  | yes      | Measurement timestamp.            |
| `data.measurements[].note`       | string  | no       | Entry note.                       |
| `data.measurements[].source`     | string  | no       | Source, for example `public_api`. |
| `data.measurements[].createdBy`  | string  | no       | Creator type.                     |

Historical access grants are applied when the client relationship has restricted
history access.

<CodeGroup>
  ```bash cURL theme={null}
  curl -H "Authorization: Bearer $FITSOCIETY_ACCESS_TOKEN" \
    "https://api.fitsociety.io/public/v1/clients/64b64c0f2f5f4c0012345678/measurements/entries?dateFrom=2026-01-01&dateTo=2026-07-13&limit=100"
  ```

  ```javascript JavaScript theme={null}
  const clientId = "64b64c0f2f5f4c0012345678";
  const query = new URLSearchParams({
    dateFrom: "2026-01-01",
    dateTo: "2026-07-13",
    limit: "100",
  });

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

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

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

  client_id = "64b64c0f2f5f4c0012345678"
  response = requests.get(
      f"https://api.fitsociety.io/public/v1/clients/{client_id}/measurements/entries",
      headers={"Authorization": f"Bearer {os.environ['FITSOCIETY_ACCESS_TOKEN']}"},
      params={"dateFrom": "2026-01-01", "dateTo": "2026-07-13", "limit": 100},
  )
  response.raise_for_status()

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

Response:

```json theme={null}
{
  "data": {
    "page": 1,
    "limit": 100,
    "total": 1,
    "totalPages": 1,
    "hasNextPage": false,
    "hasPrevPage": false,
    "measurements": [
      {
        "entryId": "64b64c0f2f5f4c0012345690",
        "typeId": "64b64c0f2f5f4c0012345691",
        "value": 82.5,
        "unit": "kg",
        "measuredAt": "2026-07-01T07:30:00.000Z",
        "note": "Morning weigh-in",
        "source": "public_api",
        "createdBy": "system"
      }
    ]
  },
  "meta": {
    "requestId": "req_0123456789abcdef",
    "rateLimit": { "limit": 10, "remaining": 9, "resetSeconds": 1 }
  }
}
```

## Create measurement entry

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

Required scope: `measurements:write`

Request body:

| Field               | Type                     | Required | Rule                                                                           |
| :------------------ | :----------------------- | :------- | :----------------------------------------------------------------------------- |
| `measurementTypeId` | string                   | yes      | Existing system, company, or client measurement type. Alias: `typeId`.         |
| `value`             | number or numeric string | yes      | Must be finite. Localized decimals are accepted. Units or labels are rejected. |
| `unit`              | string                   | no       | Must be in the measurement type `allowedUnits`. Defaults to `baseUnit`.        |
| `measuredAt`        | date/date-time           | yes      | Valid date, future dates rejected. Prefer `YYYY-MM-DD` or ISO UTC.             |
| `note`              | string                   | no       | Maximum 1000 characters.                                                       |

Accepted numeric examples:

```json theme={null}
{ "value": 82.5 }
{ "value": "82,5" }
{ "value": "1.250,5" }
```

Rejected numeric example:

```json theme={null}
{ "value": "82.5kg" }
```

Response fields:

| Field                         | Type   | Nullable | Description                   |
| :---------------------------- | :----- | :------- | :---------------------------- |
| `data.measurement.entryId`    | string | no       | Created entry ID.             |
| `data.measurement.typeId`     | string | yes      | Measurement type ID.          |
| `data.measurement.value`      | number | yes      | Stored numeric value.         |
| `data.measurement.unit`       | string | no       | Stored unit.                  |
| `data.measurement.measuredAt` | string | yes      | Stored measurement timestamp. |
| `data.measurement.note`       | string | no       | Stored note.                  |
| `data.measurement.source`     | string | no       | `public_api`.                 |
| `data.measurement.createdBy`  | string | no       | `system`.                     |

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.fitsociety.io/public/v1/clients/64b64c0f2f5f4c0012345678/measurements/entries" \
    -H "Authorization: Bearer $FITSOCIETY_ACCESS_TOKEN" \
    -H "Idempotency-Key: measurement-weight-jane-20260713" \
    -H "Content-Type: application/json" \
    -d '{
      "measurementTypeId": "64b64c0f2f5f4c0012345691",
      "value": 82.5,
      "unit": "kg",
      "measuredAt": "2026-07-13",
      "note": "Morning weigh-in"
    }'
  ```

  ```javascript JavaScript theme={null}
  const clientId = "64b64c0f2f5f4c0012345678";
  const response = await fetch(
    `https://api.fitsociety.io/public/v1/clients/${clientId}/measurements/entries`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.FITSOCIETY_ACCESS_TOKEN}`,
        "Idempotency-Key": "measurement-weight-jane-20260713",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        measurementTypeId: "64b64c0f2f5f4c0012345691",
        value: 82.5,
        unit: "kg",
        measuredAt: "2026-07-13",
        note: "Morning weigh-in",
      }),
    },
  );

  const body = await response.json();
  if (response.status === 201) {
    console.log("Created entry", body.data.measurement.entryId);
  }
  ```

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

  client_id = "64b64c0f2f5f4c0012345678"
  response = requests.post(
      f"https://api.fitsociety.io/public/v1/clients/{client_id}/measurements/entries",
      headers={
          "Authorization": f"Bearer {os.environ['FITSOCIETY_ACCESS_TOKEN']}",
          "Idempotency-Key": "measurement-weight-jane-20260713",
      },
      json={
          "measurementTypeId": "64b64c0f2f5f4c0012345691",
          "value": 82.5,
          "unit": "kg",
          "measuredAt": "2026-07-13",
          "note": "Morning weigh-in",
      },
  )
  response.raise_for_status()

  body = response.json()
  if response.status_code == 201:
      print("Created entry", body["data"]["measurement"]["entryId"])
  ```
</CodeGroup>

Response (`201 Created`):

```json theme={null}
{
  "data": {
    "measurement": {
      "entryId": "64b64c0f2f5f4c0012345690",
      "typeId": "64b64c0f2f5f4c0012345691",
      "value": 82.5,
      "unit": "kg",
      "measuredAt": "2026-07-13T00:00:00.000Z",
      "note": "Morning weigh-in",
      "source": "public_api",
      "createdBy": "system"
    }
  },
  "meta": {
    "requestId": "req_0123456789abcdef",
    "rateLimit": { "limit": 10, "remaining": 8, "resetSeconds": 1 }
  }
}
```

## Update measurement entry

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

Required scope: `measurements:write`

Allowed fields: `value`, `unit`, `measuredAt`, `note`.

Validation is identical to create. At least one allowed field is required.

Response fields:

| Field                         | Type   | Nullable | Description                       |
| :---------------------------- | :----- | :------- | :-------------------------------- |
| `data.measurement.entryId`    | string | no       | Updated entry ID.                 |
| `data.measurement.typeId`     | string | yes      | Measurement type ID.              |
| `data.measurement.value`      | number | yes      | Stored numeric value.             |
| `data.measurement.unit`       | string | no       | Stored unit.                      |
| `data.measurement.measuredAt` | string | yes      | Stored measurement timestamp.     |
| `data.measurement.note`       | string | no       | Stored note.                      |
| `data.measurement.source`     | string | no       | Source, for example `public_api`. |
| `data.measurement.createdBy`  | string | no       | Creator type.                     |

## Archive measurement entry

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

Required scope: `measurements:write`

This is a soft archive only. The endpoint sets the entry as deleted for product
views and Public API reads. It does not hard-delete measurement records.

Validation:

| Parameter  | Type   | Required | Rule                                                                           |
| :--------- | :----- | :------- | :----------------------------------------------------------------------------- |
| `clientId` | string | yes      | Client must belong to the authenticated company.                               |
| `entryId`  | string | yes      | Entry must belong to that client and company and must not already be archived. |

Response fields:

| Field                        | Type    | Nullable | Description               |
| :--------------------------- | :------ | :------- | :------------------------ |
| `data.measurement.entryId`   | string  | no       | Archived entry ID.        |
| `data.measurement.archived`  | boolean | no       | Always `true` on success. |
| `data.measurement.updatedAt` | string  | yes      | Archive update timestamp. |

## List measurement progress summaries

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

Required scope: `progress_summaries:read`

Validation:

| Parameter            | Type                             | Required | Rule                                                                                 |
| :------------------- | :------------------------------- | :------- | :----------------------------------------------------------------------------------- |
| `dateFrom`           | date/date-time                   | no       | Must be paired with `dateTo`.                                                        |
| `dateTo`             | date/date-time                   | no       | Must be paired with `dateFrom`, max 366-day range.                                   |
| `typeId` / `typeIds` | string or comma-separated string | no       | Valid measurement type ObjectIds. Alias: `measurementTypeId` / `measurementTypeIds`. |

Response fields:

| Field                                             | Type      | Nullable | Description                      |
| :------------------------------------------------ | :-------- | :------- | :------------------------------- |
| `data.summaries[].measurementType.typeId`         | string    | no       | Measurement type ID.             |
| `data.summaries[].measurementType.label`          | string    | no       | Stable label.                    |
| `data.summaries[].measurementType.name.en`        | string    | no       | English name.                    |
| `data.summaries[].measurementType.name.nl`        | string    | no       | Dutch name.                      |
| `data.summaries[].measurementType.baseUnit`       | string    | no       | Base unit.                       |
| `data.summaries[].measurementType.allowedUnits[]` | string\[] | no       | Allowed units.                   |
| `data.summaries[].totalEntries`                   | integer   | no       | Entries included for this type.  |
| `data.summaries[].latest.entryId`                 | string    | yes      | Latest entry ID.                 |
| `data.summaries[].latest.value`                   | number    | yes      | Latest value.                    |
| `data.summaries[].latest.unit`                    | string    | no       | Latest unit.                     |
| `data.summaries[].latest.measuredAt`              | string    | yes      | Latest measurement timestamp.    |
| `data.summaries[].previous.entryId`               | string    | yes      | Previous entry ID.               |
| `data.summaries[].previous.value`                 | number    | yes      | Previous value.                  |
| `data.summaries[].previous.unit`                  | string    | no       | Previous unit.                   |
| `data.summaries[].previous.measuredAt`            | string    | yes      | Previous measurement timestamp.  |
| `data.summaries[].trend.direction`                | string    | no       | `up`, `down`, `same`, or `none`. |
| `data.summaries[].trend.delta`                    | number    | yes      | Latest minus previous.           |
| `data.summaries[].trend.percentageDelta`          | number    | yes      | Percentage delta from previous.  |

Not exposed: notes, provider source refs, derivation refs, health quality flags,
coach IDs, company IDs, suppression metadata, or raw health-processing context.

## List progress photos

```http theme={null}
GET /public/v1/clients/{clientId}/progress-photos?orientation=front
Authorization: Bearer <access_token>
```

Required scope: `progress_photos:read`

Validation:

| Parameter     | Type           | Required | Rule                                               |
| :------------ | :------------- | :------- | :------------------------------------------------- |
| `page`        | integer        | no       | Minimum `1`.                                       |
| `limit`       | integer        | no       | Default `20`, maximum `100`.                       |
| `dateFrom`    | date/date-time | no       | Must be paired with `dateTo`.                      |
| `dateTo`      | date/date-time | no       | Must be paired with `dateFrom`, max 366-day range. |
| `orientation` | string         | no       | `front`, `side`, `back`, or `other`.               |

Response fields:

| Field                                  | Type    | Nullable | Description                            |
| :------------------------------------- | :------ | :------- | :------------------------------------- |
| `data.photos[].progressPhotoId`        | string  | no       | Progress photo ID.                     |
| `data.photos[].clientId`               | string  | no       | Client ID.                             |
| `data.photos[].orientation`            | string  | no       | `front`, `side`, `back`, or `other`.   |
| `data.photos[].customLabel`            | string  | no       | Custom label.                          |
| `data.photos[].image.downloadUrl`      | string  | no       | Signed/normalized media URL.           |
| `data.photos[].image.expiresAt`        | string  | yes      | URL expiry timestamp.                  |
| `data.photos[].image.expiresInSeconds` | integer | no       | Current TTL in seconds, default `900`. |
| `data.photos[].takenAt`                | string  | yes      | Photo taken timestamp.                 |
| `data.photos[].note`                   | string  | no       | Photo note.                            |
| `data.photos[].createdAt`              | string  | yes      | Creation timestamp.                    |
| `data.photos[].updatedAt`              | string  | yes      | Last update timestamp.                 |
| `data.pagination.page`                 | integer | no       | Current page.                          |
| `data.pagination.limit`                | integer | no       | Page size.                             |
| `data.pagination.totalRecords`         | integer | no       | Total photos.                          |
| `data.pagination.totalPages`           | integer | no       | Total pages.                           |
| `data.pagination.hasNextPage`          | boolean | no       | Next page availability.                |
| `data.pagination.hasPrevPage`          | boolean | no       | Previous page availability.            |

Not exposed: raw `imageUrl`, S3 keys, `createdBy`, `coachId`, `companyId`,
dimensions, file size, media provider metadata, or hard-delete controls.

## Get progress photo

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

Required scope: `progress_photos:read`

Response fields:

| Field                               | Type    | Nullable | Description                              |
| :---------------------------------- | :------ | :------- | :--------------------------------------- |
| `data.photo.progressPhotoId`        | string  | no       | Progress photo ID.                       |
| `data.photo.clientId`               | string  | no       | Client ID.                               |
| `data.photo.orientation`            | string  | no       | `front`, `side`, `back`, or `other`.     |
| `data.photo.customLabel`            | string  | no       | Custom label.                            |
| `data.photo.image.downloadUrl`      | string  | no       | Short-lived signed/normalized media URL. |
| `data.photo.image.expiresAt`        | string  | yes      | URL expiry timestamp.                    |
| `data.photo.image.expiresInSeconds` | integer | no       | Current TTL in seconds, default `900`.   |
| `data.photo.takenAt`                | string  | yes      | Photo taken timestamp.                   |
| `data.photo.note`                   | string  | no       | Photo note.                              |
| `data.photo.createdAt`              | string  | yes      | Creation timestamp.                      |
| `data.photo.updatedAt`              | string  | yes      | Last update timestamp.                   |

The detail endpoint applies the same client access, historical progress-photo
access, orientation, and date-range filters as the list endpoint. It never
returns raw storage keys or the persisted `imageUrl`.

## Not exposed in v1

| Operation                       | Reason                                                                 |
| :------------------------------ | :--------------------------------------------------------------------- |
| Hard-delete measurement entry   | Public API only exposes soft archive.                                  |
| Create/update measurement types | Company/coach configuration, not integration data entry.               |
| Progress photo upload           | Requires multipart/media validation and storage policy.                |
| Progress photo delete           | Existing internal flows delete media and database records.             |
| Objective writes                | Existing objective data includes broader health and nutrition context. |
