> ## 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.

# The data model

> How patients, appointments, invoices and payments relate — and what's derived rather than stored

The API surfaces the same records PracticeHub itself runs on. Everything below reads directly off the resource definitions and the write services — this page is the shape of the model, not a restatement of `GET /schema` (which is the byte-for-byte contract for any one resource).

## Ids and tenancy

Every id — `id`, and every `*_id` foreign key — is a plain integer, scoped to your account. Every underlying table carries a client column (`clientID` / `client_id`) that every query is filtered on; an id from another clinic's account simply doesn't exist as far as your key is concerned, and referencing one returns `404` or the relevant `*_not_found` code, never another account's record. There is no cross-account id space to guess at.

## The shape of it

```mermaid theme={null}
erDiagram
    PATIENTS ||--o{ APPOINTMENTS : "patient_id"
    PATIENTS ||--o{ INVOICES : "patient_id"
    PATIENTS ||--o{ PAYMENTS : "patient_id"
    PATIENTS ||--o{ FILES : "patient_id"
    PATIENTS ||--o{ PATIENT_LOGS : "patient_id"
    PATIENTS ||--o| NUMBERS : "embedded on read"
    PATIENTS ||--o| ADDRESSES : "embedded on read"

    APPOINTMENTS }o--|| PRACTITIONERS : "practitioner_id"
    APPOINTMENTS }o--|| LOCATIONS : "location_id"
    APPOINTMENTS }o--|| APPOINTMENT_TYPES : "appointment_type_id"
    APPOINTMENTS }o--o| RESOURCES : "resource_id"
    APPOINTMENTS ||--o{ INVOICES : "appointment_id"

    INVOICES ||--o{ LINE_ITEMS : "invoice_id"
    INVOICES ||--o{ PAYMENT_ALLOCATIONS : "invoice_id"
    PAYMENTS ||--o{ PAYMENT_ALLOCATIONS : "payment_id"

    FILES }o--o| PATIENT_LOGS : "patient_log_id (file_type=patient_log)"

    DELETED_ENTITIES }o..o{ PATIENTS : "tombstone: entity_type + entity_id"
```

A few relationships worth being explicit about:

* **`appointments`** point at `patient_id`, `practitioner_id`, `location_id` and `appointment_type_id` — all required to book — plus an optional `resource_id` (required only when the location is resource-view, `locations.calendar_view = 1`) and `group_master_id` for a group session attendee. See [Writing → Appointments](/guides/writing).
* **`invoices`** belong to a `patient_id` and optionally an `appointment_id`, `practitioner_id` and `location_id`; every invoice owns one or more `line_items`, each pointing at a `billable_item_id` (a service/product from PracticeHub's catalogue) and a `provider_id`.
* **`payments`** belong to a `patient_id` and a `payment_method_id` (a row in `payment_methods`, or one of the account's negative-id special types — credit, write-off).
* **`care_plans`** belong to a `patient_id` (the account holder) and carry three embedded views: `billing` (how the plan is paid for — type, interval, `total_price`), `schedules[]` (one visit schedule per patient on the plan) and `members[]` (every patient sharing it). Read-only: a plan's appointments are ordinary `appointments` and its charges ordinary `invoices`.
* **`payment_allocations`** are the join between the two ledgers: each one ties one `payment_id` to one `invoice_id` for an `amount`. The request's `patient_id` must be the **invoice's** owner — but the payment may belong to a different patient, deliberately: family accounts and group care plans settle one member's invoice from another's payment, and both patients' balances are refreshed. See [Billing → Allocations](/resources-guide/billing).
* **`files`** attach to a `patient_id`; a file of `file_type: patient_log` additionally carries `patient_log_id`, pointing at one of that patient's own log entries.
* **Reference data** — `practitioners`, `locations`, `appointment_types`, `modalities`, `resources`, `payment_methods`, `referral_sources`, `billable_items` — is what appointments, invoices and payments point at. It's read-only (or no-delete) over the API today; see [Resources](/resources) for exactly which.

## Patients embed their contact details on read

`numbers` and `addresses` are resources in their own right (read-only as such — see [Resources](/resources)), but every `GET /patients` and `GET /patients/{id}` embeds them directly:

```json theme={null}
{
  "id": 340,
  "first_name": "Ada",
  "last_name": "Lovelace",
  "numbers": [
    { "id": 12, "number": "+44 7700 900123", "country_code": "44", "iso2": "gb", "type": "mobile", "intl_number": "+447700900123" }
  ],
  "address": { "line_1": "12 Analytical Way", "line_2": null, "city": "London", "state": null, "postal_code": "N1 9GU", "country": "GB" },
  "metadata": {}
}
```

A patient round-trips in one request: read it, edit `numbers[]` or `address`, `PATCH` it back. Numbers are reconciled by `id` — send the `id` you read back to keep or update an entry, omit it to add one, leave a stored one out to remove it. `address` is a single object (one address per patient on the API); sending specific keys changes only those keys. Both are loaded per page, not per row, so listing patients doesn't cost a query per patient. Full write rules: [Writing → Patients](/guides/writing).

## The ledger is derived, not entered

`invoices.balance`, `invoices.patient_balance`, `invoices.third_party_balance` and the equivalent balances on `patients` are computed by PracticeHub's billing service from the invoice's line items and whatever has been allocated against it from `payments` via `payment_allocations` — the same arithmetic the app's own invoice screen runs. They are ordinary attributes you can read and filter and sort on, but no write path accepts them as input: `POST /invoices` and `PATCH /invoices/{id}` take `line_items`, not a balance, and `POST /payment_allocations` is how a payment gets applied. There is no way to set a balance directly, on create or update — you supply the facts (lines, payments, allocations) and the ledger is always the result of those facts, never a value you assert.

This is also why data migrations import in a fixed order — patients, then appointments, then invoices, then payments, then allocations — so that by the time an old balance needs to reappear, it does so as an emergent property of the records that produced it. See [Data migration → Import order](/guides/data-migration#import-order).

## `deleted_entities`: the tombstone feed

Records deleted in PracticeHub — through the API, the app, or a background job — disappear from their own resource, but a row survives in `deleted_entities`: `entity_type`, `entity_id`, `patient_id` (when applicable), and `created_at` / `updated_at` (the deleted record's own original stamps) and `deleted_at` (when it was deleted). It's read-only and exists for exactly one job: let a sync ask "what has disappeared since I last looked" with `GET /deleted_entities?deleted_at=gte:{last sync}` instead of diffing full listings. See [Querying → Deleted records](/guides/querying#deleted-records).

## `metadata` on everything

Every resource carries a `metadata` object — your own identifiers, up to 50 keys, filterable with the same operators as any other attribute (`metadata[key]=eq:value`). It's how you attach a CRM contact id, a legacy record id, or a sync cursor to a PracticeHub record without keeping a separate mapping table. Full details: [Metadata](/guides/metadata).


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