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

# List Profiles

> List or look up patient profiles by email

List patient profiles accessible to your API key, or look one up by exact email address.

When `email` is provided, the endpoint performs a Redis-cached exact-match lookup against the
canonical sign-in email and returns 0 or 1 result; `limit` and `offset` are ignored in that
case. When omitted, the endpoint returns a paginated list of profiles scoped to your API
key's access context.

Email matching is case-insensitive and exact — substring and prefix matches are not supported.
Only the canonical sign-in email is checked; alternate addresses such as `work_email` are not
considered.

<Note>Requires `profiles_view` or `impersonate` scope.</Note>

## Request

### Query parameters

<ParamField query="email" type="string">
  Filter by exact email address (case-insensitive). When set, returns 0 or 1 result and
  `limit`/`offset` are ignored. The lookup is served by a Redis-cached reverse index, with an
  indexed SQL fallback.
</ParamField>

<ParamField query="limit" type="integer" default="100">
  Maximum number of profiles to return. Defaults to 100, maximum 500. Ignored when `email` is set.
</ParamField>

<ParamField query="offset" type="integer" default="0">
  Number of profiles to skip for pagination. Defaults to 0. Ignored when `email` is set.
</ParamField>

## Response

On success, the API returns `200 OK` with an array of profile objects. The shape of each
entry matches [Get Profile](/api/profiles/get-profile).

<ResponseField name="id" type="string" required>
  Unique profile identifier (UUID).
</ResponseField>

<ResponseField name="handle" type="string">
  Profile handle or username. May be `null`.
</ResponseField>

<ResponseField name="first_name" type="string">
  First name. May be `null`.
</ResponseField>

<ResponseField name="last_name" type="string">
  Last name. May be `null`.
</ResponseField>

<ResponseField name="email" type="string">
  Email address. May be `null`.
</ResponseField>

<ResponseField name="phone" type="string">
  Phone number in E.164 format. May be `null`.
</ResponseField>

<ResponseField name="sex" type="integer">
  Biological sex per ISO/IEC 5218 (`0` = unknown, `1` = male, `2` = female). May be `null`.
</ResponseField>

<ResponseField name="date_of_birth" type="string">
  Date of birth in `YYYY-MM-DD` format. May be `null`.
</ResponseField>

<ResponseField name="height" type="number">
  Height in centimeters. May be `null`.
</ResponseField>

<ResponseField name="weight" type="number">
  Weight in kilograms. May be `null`.
</ResponseField>

<ResponseField name="language" type="string">
  Preferred language (`en`, `de`, or `fi`).
</ResponseField>

<ResponseField name="transactional_emails_enabled" type="boolean">
  Whether this profile receives transactional (programmatic) emails. `null` when never set.
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO 8601 timestamp of when the profile was created.
</ResponseField>

<ResponseField name="updated_at" type="string">
  ISO 8601 timestamp of the last update. May be `null`.
</ResponseField>

## Behaviour notes

* An empty array is returned both when no profile matches the email and when a match exists
  outside your access context. This is intentional — it prevents the endpoint from leaking the
  existence of profiles you cannot read.
* Newly created profiles are immediately discoverable by `email` because the create flow
  warms the cache synchronously.
* The unfiltered list is ordered by `created_at` descending (newest first) and paginated via
  `limit` and `offset`.

## Error responses

| Status | Description                                                                                |
| ------ | ------------------------------------------------------------------------------------------ |
| `400`  | Validation error — invalid email format, or `limit`/`offset` out of range.                 |
| `403`  | Forbidden — your API key does not have the required scope (`profiles_view`/`impersonate`). |
| `500`  | Internal server error.                                                                     |

<RequestExample>
  ```bash cURL (lookup by email) theme={null}
  curl --request GET \
    --url 'https://anivahealth.com/api/v1/profiles?email=maria.schmidt@example.com' \
    --header 'x-api-key: YOUR_API_KEY'
  ```

  ```bash cURL (paginated list) theme={null}
  curl --request GET \
    --url 'https://anivahealth.com/api/v1/profiles?limit=50&offset=0' \
    --header 'x-api-key: YOUR_API_KEY'
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  [
    {
      "id": "a3f1c2d4-8b7e-4f2a-9c1d-2e3f4a5b6c7d",
      "handle": null,
      "first_name": "Maria",
      "last_name": "Schmidt",
      "email": "maria.schmidt@example.com",
      "phone": "+4930123456789",
      "sex": 2,
      "date_of_birth": "1985-03-22",
      "height": 168,
      "weight": 65,
      "language": "de",
      "transactional_emails_enabled": true,
      "created_at": "2026-04-01T09:14:32Z",
      "updated_at": "2026-04-01T11:05:00Z"
    }
  ]
  ```

  ```json 200 (no match) theme={null}
  []
  ```

  ```json 400 theme={null}
  {
    "error": "email: Invalid email format"
  }
  ```
</ResponseExample>
