---
title: "Patients API reference — fields and endpoints"
description: "Field-level reference for the HyperCRM patients resource: the patient and note object shapes, each endpoint's parameters, and the errors returned."
canonical: "https://hypercrm.app/docs/api/patients"
lang: "en"
updated: "2026-07-26"
---

_API reference / Patients_

# Patients and patient notes

The patient record, per-patient clinical notes with revision history, and tag toggling. Every field, every endpoint, and the exact errors each one returns.

[Start the free trial](/dashboard) · [Back to API reference](/docs/api)

> **Status is not a fixed enum** — Valid patient status values come from the practice's current pack, not from a list fixed by the API. Read them from GET /api/v1/me rather than hardcoding them, or a practice on a different pack will reject writes your client believes are valid.

## Scoping and identifiers

Every endpoint here requires authentication and operates scoped to the caller's own practice — a request never supplies a practice id, and cannot reach another practice's records by asking.

Path segments (`patientId`, `noteId`, `tagId`) must be canonical UUIDs. A malformed id returns `not_found` (404) without ever reaching the data layer, so a malformed id and a valid-but-nonexistent one are indistinguishable to the caller. That is deliberate: it stops the API confirming which ids exist.

## The patient object

```json
{
  "id": "0f1c7a2e-....",
  "practiceId": "8b3d1f60-....",
  "firstName": "Jo",
  "lastName": "Doe",
  "dob": "1990-05-14T00:00:00.000Z",
  "gender": null,
  "email": null,
  "phoneMobile": null,
  "address": null,
  "postcode": null,
  "country": null,
  "status": "active",
  "statusChangedAt": "2026-01-01T00:00:00.000Z",
  "tagIds": [],
  "notifyByEmail": true,
  "notifyBySms": true,
  "metadata": {},
  "createdAt": "2026-01-01T00:00:00.000Z",
  "updatedAt": "2026-01-01T00:00:00.000Z"
}
```

`dob`, `statusChangedAt`, `createdAt` and `updatedAt` are ISO 8601 strings; `null` stays `null`. Note that `dob` is accepted as `YYYY-MM-DD` on write and returned as a full timestamp on read.

## The note object

```json
{
  "id": "c41a8e93-....",
  "patientId": "0f1c7a2e-....",
  "practiceId": "8b3d1f60-....",
  "title": null,
  "body": "Called patient to confirm appointment.",
  "isNext": false,
  "revisions": [],
  "createdBy": "3d9b2c15-....",
  "createdAt": "2026-01-01T00:00:00.000Z",
  "updatedAt": "2026-01-01T00:00:00.000Z"
}
```

`revisions` accumulates prior versions of the note every time it is edited — an audit trail, not an opt-in feature. `createdBy` is the id of the user who wrote it.

## Endpoints

### GET /api/v1/patients

Lists patients in the caller's practice, newest fields included, with optional search and filters.

Out-of-range pagination values are clamped rather than rejected: limit=5000 returns 200 results, it does not fail. The response echoes the effective limit and offset actually used, so a client can detect that clamping happened.

**Query parameters**

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `limit` | `integer` | `50` | Clamped to 1–200. A non-numeric value falls back to the default. |
| `offset` | `integer` | `0` | Clamped to a minimum of 0. No upper bound. |
| `search` | `string` | — | Matched against name fields. Wildcards in the input are escaped, not interpreted. |
| `status` | `string` | — | Filters by a status id defined by the practice's pack. |
| `tagId` | `string` | — | Filters to patients carrying this tag. |

**Response · 200**

```json
{
  "data": {
    "patients": [
      { "id": "0f1c7a2e-....", "firstName": "Jo", "lastName": "Doe" }
    ],
    "limit": 50,
    "offset": 0
  },
  "requestId": "req_5f2c1e40-...."
}
```

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `missing_authorization` | 401 | No Authorization header was sent. |

### POST /api/v1/patients

Creates a patient.

The body is strict: an unrecognized field is rejected rather than ignored, so a typo in a field name fails loudly instead of silently dropping data.

**Body**

| Field | Type | Description |
| --- | --- | --- |
| `firstName` (Required) | `string` | Trimmed; must not be empty. |
| `lastName` (Required) | `string` | Trimmed; must not be empty. |
| `dob` | `string \| null` | YYYY-MM-DD, and must be a real calendar date. |
| `gender` | `string \| null` | Free text. |
| `email` | `string \| null` | Must be a valid address if present. |
| `phoneMobile` | `string \| null` | Free text. |
| `address` | `string \| null` | Free text. |
| `postcode` | `string \| null` | Free text. |
| `country` | `string \| null` | Free text. |
| `status` | `string` | Must be a status id from the practice's pack. Validated against the pack, not a fixed list. |
| `notifyByEmail` | `boolean` | Whether this patient may be emailed. |
| `notifyBySms` | `boolean` | Whether this patient may be texted. |

**Example request**

```json
{
  "firstName": "Jo",
  "lastName": "Doe",
  "dob": "1990-05-14",
  "status": "active"
}
```

**Response · 201**

```json
{
  "data": {
    "id": "0f1c7a2e-....",
    "firstName": "Jo",
    "lastName": "Doe",
    "dob": "1990-05-14T00:00:00.000Z",
    "status": "active",
    "createdAt": "2026-01-01T00:00:00.000Z",
    "updatedAt": "2026-01-01T00:00:00.000Z"
  },
  "requestId": "req_5f2c1e40-...."
}
```

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `validation_failed` | 400 | A required field is missing, dob or email is malformed, or the body carries an unrecognized field. details names the offending path. |
| `validation_failed` | 400 | status is not one of the practice pack's values. |

### GET /api/v1/patients/:patientId

Fetches one patient.

**Path parameters**

| Field | Type | Description |
| --- | --- | --- |
| `patientId` (Required) | `uuid` | Canonical UUID. |

**Response · 200**

```json
{
  "data": { "id": "0f1c7a2e-....", "firstName": "Jo", "lastName": "Doe" },
  "requestId": "req_5f2c1e40-...."
}
```

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `not_found` | 404 | The id is not a UUID, does not exist, or belongs to another practice — all three are indistinguishable. |

### PATCH /api/v1/patients/:patientId

Updates a patient. Every field from the create body, all optional.

**Path parameters**

| Field | Type | Description |
| --- | --- | --- |
| `patientId` (Required) | `uuid` | Canonical UUID. |

**Example request**

```json
{ "firstName": "Joanna" }
```

**Response · 200**

```json
{
  "data": { "id": "0f1c7a2e-....", "firstName": "Joanna" },
  "requestId": "req_5f2c1e40-...."
}
```

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `validation_failed` | 400 | Same rules as creating a patient. |
| `not_found` | 404 | No such patient in this practice. |

### DELETE /api/v1/patients/:patientId

Deletes a patient.

**Path parameters**

| Field | Type | Description |
| --- | --- | --- |
| `patientId` (Required) | `uuid` | Canonical UUID. |

**Response · 200**

```json
{
  "data": { "deleted": true },
  "requestId": "req_5f2c1e40-...."
}
```

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `not_found` | 404 | No such patient in this practice. |

### POST /api/v1/patients/:patientId/tags/:tagId

Toggles a tag on a patient — adds it if absent, removes it if present.

There is no request body, and no separate remove endpoint: the tag id comes from the path and the call flips its state. Sending the same request twice returns the patient to where it started.

**Path parameters**

| Field | Type | Description |
| --- | --- | --- |
| `patientId` (Required) | `uuid` | Canonical UUID. |
| `tagId` (Required) | `uuid` | Canonical UUID of a tag in this practice. |

**Response · 200**

```json
{
  "data": { "id": "0f1c7a2e-....", "tagIds": ["b7e42d18-...."] },
  "requestId": "req_5f2c1e40-...."
}
```

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `not_found` | 404 | The patient or the tag does not exist in this practice. |

### GET /api/v1/patients/:patientId/notes

Lists a patient's notes.

**Path parameters**

| Field | Type | Description |
| --- | --- | --- |
| `patientId` (Required) | `uuid` | Canonical UUID. |

**Response · 200**

```json
{
  "data": { "notes": [] },
  "requestId": "req_5f2c1e40-...."
}
```

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `not_found` | 404 | No such patient in this practice. |

### POST /api/v1/patients/:patientId/notes

Adds a note to a patient.

**Path parameters**

| Field | Type | Description |
| --- | --- | --- |
| `patientId` (Required) | `uuid` | Canonical UUID. |

**Body**

| Field | Type | Description |
| --- | --- | --- |
| `body` (Required) | `string` | Trimmed; must not be empty. |
| `title` | `string \| null` | Optional heading for the note. |
| `isNext` | `boolean` | Marks the note as the next action for this patient. |

**Example request**

```json
{
  "body": "Called patient to confirm appointment.",
  "isNext": true
}
```

**Response · 201**

```json
{
  "data": {
    "id": "c41a8e93-....",
    "body": "Called patient to confirm appointment.",
    "isNext": true,
    "createdAt": "2026-01-01T00:00:00.000Z",
    "updatedAt": "2026-01-01T00:00:00.000Z"
  },
  "requestId": "req_5f2c1e40-...."
}
```

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `validation_failed` | 400 | body is missing or empty, or the body carries an unrecognized field. |
| `not_found` | 404 | No such patient in this practice. |

### PATCH /api/v1/patients/:patientId/notes/:noteId

Edits a note. The previous version is kept in revisions.

**Path parameters**

| Field | Type | Description |
| --- | --- | --- |
| `patientId` (Required) | `uuid` | Canonical UUID. |
| `noteId` (Required) | `uuid` | Canonical UUID. |

**Body**

| Field | Type | Description |
| --- | --- | --- |
| `title` | `string \| null` | Optional heading. |
| `body` | `string` | If present, must still be non-empty. |
| `isNext` | `boolean` | Marks the note as the next action. |

**Example request**

```json
{ "title": "Follow-up" }
```

**Response · 200**

```json
{
  "data": { "id": "c41a8e93-....", "title": "Follow-up" },
  "requestId": "req_5f2c1e40-...."
}
```

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `validation_failed` | 400 | body is present but empty, or an unrecognized field was sent. |
| `not_found` | 404 | No such patient or note in this practice. |

### DELETE /api/v1/patients/:patientId/notes/:noteId

Deletes a note.

**Path parameters**

| Field | Type | Description |
| --- | --- | --- |
| `patientId` (Required) | `uuid` | Canonical UUID. |
| `noteId` (Required) | `uuid` | Canonical UUID. |

**Response · 200**

```json
{
  "data": { "deleted": true },
  "requestId": "req_5f2c1e40-...."
}
```

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `not_found` | 404 | No such patient or note in this practice. |

## Common questions

### How is patient data protected?

Patient records are encrypted in transit and at rest, files are stored privately and served through short-lived signed links, and every change is written to an audit log. Staff accounts sign in with passkeys rather than shared passwords.

### Is patient status a fixed set of values?

No. Valid status values come from the practice's current pack rather than a fixed API-wide enum, and are returned by GET /api/v1/me. Creating or updating a patient with a status outside that list returns validation_failed naming the offending path.

### Why does a malformed id return 404 rather than 400?

Because a malformed id and a valid id belonging to another practice must look identical from outside. If one returned 400 and the other 404, the difference would let a caller confirm which record ids exist in other practices.

### Do patient notes keep a history of edits?

Yes. Each note carries a revisions array that accumulates prior versions of its content every time it is edited, alongside the id of the user who created it. It is an audit trail rather than a versioning feature you opt into.