> ## 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` | object | no | All stored type names by language code. |
| `data.types[].defaultLanguage` | string | yes | `null` until the source records declare a default language. |
| `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` | object | no | All stored names by language code. |
| `data.summaries[].measurementType.defaultLanguage` | string | yes | `null` until the source records declare a default language. |
| `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. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.