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.
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
{
"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
{
"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
{
"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 |
|---|---|---|
| firstNameRequired | string | Trimmed; must not be empty. |
| lastNameRequired | string | Trimmed; must not be empty. |
| dob | string | null | YYYY-MM-DD, and must be a real calendar date. |
| gender | string | null | Free text. |
| 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
{
"firstName": "Jo",
"lastName": "Doe",
"dob": "1990-05-14",
"status": "active"
}Response · 201
{
"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 |
|---|---|---|
| patientIdRequired | uuid | Canonical UUID. |
Response · 200
{
"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 |
|---|---|---|
| patientIdRequired | uuid | Canonical UUID. |
Example request
{ "firstName": "Joanna" }Response · 200
{
"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 |
|---|---|---|
| patientIdRequired | uuid | Canonical UUID. |
Response · 200
{
"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 |
|---|---|---|
| patientIdRequired | uuid | Canonical UUID. |
| tagIdRequired | uuid | Canonical UUID of a tag in this practice. |
Response · 200
{
"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 |
|---|---|---|
| patientIdRequired | uuid | Canonical UUID. |
Response · 200
{
"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 |
|---|---|---|
| patientIdRequired | uuid | Canonical UUID. |
Body
| Field | Type | Description |
|---|---|---|
| bodyRequired | 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
{
"body": "Called patient to confirm appointment.",
"isNext": true
}Response · 201
{
"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 |
|---|---|---|
| patientIdRequired | uuid | Canonical UUID. |
| noteIdRequired | 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
{ "title": "Follow-up" }Response · 200
{
"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 |
|---|---|---|
| patientIdRequired | uuid | Canonical UUID. |
| noteIdRequired | uuid | Canonical UUID. |
Response · 200
{
"data": { "deleted": true },
"requestId": "req_5f2c1e40-...."
}Errors
| Code | Status | When |
|---|---|---|
| not_found | 404 | No such patient or note in this practice. |
Common questions
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.
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.
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.
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.