Skip to main content
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

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.
  • 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.
  • 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 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), but every GET /patients and GET /patients/{id} embeds them directly:
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.

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.

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.

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.