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

# Patients

> The patient record end to end — creating, updating, contact details, referrals, blocking and deletion

`patients` is the account's core record — practitioners, appointments, invoices and files all hang off a `patient_id`. Every write runs through `PatientService`, the same code PracticeHub's own patient file uses, so a patient created by an integration is held to exactly the rules a receptionist is.

<Info>
  `numbers[]` and `address` are separate tables under the hood (`numbers`, `address`), but they are not separate resources to write — you write them nested inside a `patients` create or update, and they come back nested on every read. `GET /numbers` and `GET /addresses` exist for read-only bulk queries; the contract lives here.
</Info>

## Creating a patient

Only `first_name` and `last_name` are required — the same minimum every patient form in PracticeHub enforces:

```bash theme={null}
curl -X POST $BASE/patients \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "first_name": "Ada",
    "last_name": "Lovelace",
    "dob": "1985-12-10",
    "sex": "female",
    "email": "ada@example.org",
    "numbers": [{ "number": "+44 7700 900123", "type": "mobile" }],
    "address": { "line_1": "12 Analytical Way", "city": "London", "postal_code": "N1 9GU", "country": "GB" }
  }'
```

| Field | Notes |
| - | - |
| `first_name`, `last_name` | Required |
| `preferred_name` | Optional, ≤ 255 chars |
| `dob` | `YYYY-MM-DD` |
| `email` | Validated as an email address, ≤ 255 chars |
| `sex` | `male`, `female`, `other` or `prefer_not_to_say` — words, never a stored code |
| `note` | Free text — the patient file's general note |
| `custom_reference` | Your own reference string, ≤ 255 chars — a plain field on the patient record, distinct from `metadata` |
| `locale` | Preferred language, ≤ 255 chars |
| `default_location_id` | Must be one of the account's own locations (`GET /locations`) — `422 location_not_found` otherwise |
| `blocked` | See [Blocking online booking](#blocking-online-booking) below |

## Contact details: numbers and address

Every phone number needs a `type` — `mobile`, `home`, `work` or `other` (matched case-insensitively on input, always lowercase on output). A number sent with no type is refused (`422 phone_type_required`); a type outside that list is not refused, it's just stored as `Mobile` — historic data and free-text imports carry values PracticeHub itself never generates, and the write path doesn't punish that.

Numbers are normalised on the way in: `country_code` is stored without its leading `+`, `iso2` is lower-cased, and an E.164 `intl_number` is derived automatically when you don't supply one — send `number` and `iso2` (or a number that already has a `+countrycode` prefix) and the API works out the rest. An unparsable number is still stored, with a best-effort `intl_number` — never refused, matching what the patient-file phone widget does.

```json theme={null}
{ "number": "07700 900123", "iso2": "gb", "type": "mobile" }
```

reads back as:

```json theme={null}
{ "id": 5012, "number": "07700 900123", "country_code": "44", "iso2": "gb", "type": "mobile", "intl_number": "+447700900123" }
```

The `address` object accepts `line_1`, `line_2`, `city`, `state`, `postal_code`, `country` — all optional, all ≤ 255 chars.

### Updating numbers: read the `id` back first

`PATCH` reconciles `numbers[]` by `id`, the same pattern `invoices.line_items[]` uses:

* An entry with the **`id` you read back** is updated in place.
* An entry with **no `id`** is added.
* A stored number you **leave out entirely** is removed.

```bash theme={null}
curl -X PATCH $BASE/patients/340 \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "numbers": [
      { "id": 5012, "number": "07700 900999" },
      { "number": "020 7946 0999", "type": "Work" }
    ]
  }'
```

This keeps number `5012` (with a new digit string) and adds a new `Work` number; any third number that existed on the patient and isn't listed here is deleted. Sending an `id` that isn't one of this patient's numbers is a `422 phone_number_not_found` — read the patient first rather than guessing ids.

<Tip>
  A list with **no ids at all** still works — it replaces the whole set, but only writes anything if the new list actually differs from what's stored, so re-sending an unchanged list is a safe no-op for simple clients that don't want to track ids.
</Tip>

`address` is a true partial update: `PATCH` with only `{ "address": { "postal_code": "N1 9GH" } }` changes the postcode and leaves every other line alone — unlike `numbers[]`, there's no reconciliation to think about, because there's only ever one address row per patient.

## Tags

Every patient read carries the file's current tags:

```json theme={null}
"tags": [
  { "id": 18, "name": "VIP", "color": "#4F86C6", "patient_tag_id": 9021, "editable": true, "added_at": "2026-08-19T14:22:05Z" },
  { "id": 21, "name": "Corrective care", "color": null, "patient_tag_id": 9034, "editable": false, "added_at": "2026-09-01T08:00:00Z" }
]
```

Only active tags of the account are listed, each once. `id` is the tag (`GET /tags` lists the account's tags — `id`, `name`, `color`, `sort_order`); `patient_tag_id` is this tag on this file. `editable: false` means a care plan put the tag there — the plan takes it off when it ends.

Tags go on and come off through their own write-only resource (`POST` and `DELETE`; there is no `GET /patient_tags` — read a file's tags here), never through a patient write (`tags` on a `POST`/`PATCH /patients` is a `422`):

```bash theme={null}
curl -X POST $BASE/patient_tags \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "patient_id": 340, "tag_id": 18 }'

curl -X DELETE $BASE/patient_tags/9021 -H "Authorization: Bearer $KEY"
```

`tag_id` must be an active tag of the account (`422 tag_not_found`) and `patient_id` a live patient (`422 patient_not_found`). Putting on a tag the file already holds returns the existing row rather than a second one. Taking off a tag a care plan manages is refused (`409 tag_locked`). Either change announces `patients.updated` to your webhooks.

## Referral sources

`referral_source_id` and `referral_source_type` travel together — send both or neither. `referral_source_type` is `referral_source` (a source from `GET /referral_sources`, must be active) or `patient` (another patient on the account, must be live and not this patient — `422 referral_self` if it is).

```json theme={null}
{ "referral_source_id": 12, "referral_source_type": "referral_source" }
```

PracticeHub's public intake forms can record a patient referral by name alone, for staff to match to a real patient record later — that "referred by *Jane*, unmatched" state exists because a public form has no patient list to pick from. An API integration does have one: **a patient referral written through the API must always name the referring patient's id.** `referral_source_type: "patient"` with no `referral_source_id` is refused (`422 referral_patient_required`), not left half-filled.

Changing an unrelated field doesn't force you to re-validate a referral you didn't touch — a reference the update leaves alone is kept even if the referral source has since been deactivated.

## Communication preferences

Only two notification toggles are exposed as patient attributes today, nested under `communication_preferences`:

```json theme={null}
{ "communication_preferences": { "marketing": { "push_notification": false }, "transactional": { "push_notification": true } } }
```

Marketing email/SMS opt-in, the reminder/confirmation channel and other comms settings are account defaults and app-level preferences, not attributes this resource writes — `GET /schema/patients` is the source of truth for exactly what's accepted.

## Blocking online booking

`blocked: true` is the patient file's own **Block Online Booking** toggle — it records that this patient shouldn't be offered online self-booking. It is a plain flag on the record, not a rule this resource enforces: nothing in `POST /appointments` checks it, so a booking made through this API for a blocked patient succeeds exactly as it would for any other patient.

## Duplicate strategy

There's no built-in duplicate check on `POST /patients` — the same patient sent twice creates two records, so dedupe before you write:

* **Migrating from another system?** Use the [legacy-ID pattern](/guides/data-migration#the-legacy-id-pattern) — a `metadata.legacy_id` and a `GET /patients?metadata[legacy_id]=eq:…` lookup before every create. This is the recommended approach whenever you control the identifier.
* **No stable external id?** Query by the attributes that are actually filterable on this resource — `email` or name:

```bash theme={null}
curl -g "$BASE/patients?email=eq:ada@example.org" -H "Authorization: Bearer $KEY"
curl -g "$BASE/patients?first_name=eq:Ada&last_name=eq:Lovelace" -H "Authorization: Bearer $KEY"
```

Neither is a guarantee — patients share names, and email is optional — so treat a match as a candidate to confirm (against `dob`, phone) rather than an automatic merge.

## Deletion semantics

`DELETE /patients/{id}` is the same soft delete the patient file's own delete action performs, in one transaction:

1. The patient row is flagged `deleted` (not removed) and its mobile-app login is disconnected.
2. Every recall for the patient is deactivated.
3. **Every future `pending` appointment for the patient is deleted too** — the same rule [appointment delete](/resources-guide/appointments#deleting-an-appointment) applies, so a pending visit that's already carrying an active invoice (a late-reschedule fee, say) is left behind rather than silently dropped.
4. A patient-log entry records the deletion, and the patient is removed from search.

A snapshot — `first_name`, the last name's first initial, and `default_location_id` — lands in `deleted_entities` before the flag is set, so a sync process that reads `deleted_entities` can still tell who a deleted id used to be. Deleting an already-deleted patient is a `404`, not a no-op success.

See [Writing](/guides/writing#the-apps-rules-apply) for the full field-by-field contract and error codes, and [Data migration](/guides/data-migration#1-patients) for importing patients in bulk.


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