Skip to content

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

FieldTypeDefaultDescription
limitinteger50Clamped to 1–200. A non-numeric value falls back to the default.
offsetinteger0Clamped to a minimum of 0. No upper bound.
searchstringMatched against name fields. Wildcards in the input are escaped, not interpreted.
statusstringFilters by a status id defined by the practice's pack.
tagIdstringFilters to patients carrying this tag.

Response · 200

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

Errors

CodeStatusWhen
missing_authorization401No 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

FieldTypeDescription
firstNameRequiredstringTrimmed; must not be empty.
lastNameRequiredstringTrimmed; must not be empty.
dobstring | nullYYYY-MM-DD, and must be a real calendar date.
genderstring | nullFree text.
emailstring | nullMust be a valid address if present.
phoneMobilestring | nullFree text.
addressstring | nullFree text.
postcodestring | nullFree text.
countrystring | nullFree text.
statusstringMust be a status id from the practice's pack. Validated against the pack, not a fixed list.
notifyByEmailbooleanWhether this patient may be emailed.
notifyBySmsbooleanWhether 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

CodeStatusWhen
validation_failed400A required field is missing, dob or email is malformed, or the body carries an unrecognized field. details names the offending path.
validation_failed400status is not one of the practice pack's values.

GET/api/v1/patients/:patientId

Fetches one patient.

Path parameters

FieldTypeDescription
patientIdRequireduuidCanonical UUID.

Response · 200

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

Errors

CodeStatusWhen
not_found404The 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

FieldTypeDescription
patientIdRequireduuidCanonical UUID.

Example request

{ "firstName": "Joanna" }

Response · 200

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

Errors

CodeStatusWhen
validation_failed400Same rules as creating a patient.
not_found404No such patient in this practice.

DELETE/api/v1/patients/:patientId

Deletes a patient.

Path parameters

FieldTypeDescription
patientIdRequireduuidCanonical UUID.

Response · 200

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

Errors

CodeStatusWhen
not_found404No 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

FieldTypeDescription
patientIdRequireduuidCanonical UUID.
tagIdRequireduuidCanonical UUID of a tag in this practice.

Response · 200

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

Errors

CodeStatusWhen
not_found404The patient or the tag does not exist in this practice.

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

Lists a patient's notes.

Path parameters

FieldTypeDescription
patientIdRequireduuidCanonical UUID.

Response · 200

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

Errors

CodeStatusWhen
not_found404No such patient in this practice.

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

Adds a note to a patient.

Path parameters

FieldTypeDescription
patientIdRequireduuidCanonical UUID.

Body

FieldTypeDescription
bodyRequiredstringTrimmed; must not be empty.
titlestring | nullOptional heading for the note.
isNextbooleanMarks 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

CodeStatusWhen
validation_failed400body is missing or empty, or the body carries an unrecognized field.
not_found404No such patient in this practice.

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

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

Path parameters

FieldTypeDescription
patientIdRequireduuidCanonical UUID.
noteIdRequireduuidCanonical UUID.

Body

FieldTypeDescription
titlestring | nullOptional heading.
bodystringIf present, must still be non-empty.
isNextbooleanMarks the note as the next action.

Example request

{ "title": "Follow-up" }

Response · 200

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

Errors

CodeStatusWhen
validation_failed400body is present but empty, or an unrecognized field was sent.
not_found404No such patient or note in this practice.

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

Deletes a note.

Path parameters

FieldTypeDescription
patientIdRequireduuidCanonical UUID.
noteIdRequireduuidCanonical UUID.

Response · 200

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

Errors

CodeStatusWhen
not_found404No 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.