Skip to main content
Every resource is a uniform collection under https://{clinic}.{region}.practicehub.io/v3/api:

Filters

Filter with ?attribute=operator:value. Any filterable attribute (see GET /schema/{resource}) accepts any operator; repeat an attribute to AND several conditions.
Dates and times take the shape the attribute reads back in: clinic wall time with no zone (starts_at=gte:2026-09-01T00:00:00), UTC stamps with Z (updated_at=gte:2026-09-01T00:00:00Z; an offset is accepted and converted), calendar days as YYYY-MM-DD — see Dates. Attributes the API speaks in words (patients.sex: male, female, other) are filtered by those words — sex=eq:female, sex=in:male,female — never by a stored code; a word outside the set is a 422.
Filtering on an attribute the resource does not expose, or an unsortable attribute, is a 422 with the attribute named in errors — nothing is silently ignored.

Metadata filters

Your own identifiers stored in metadata are filterable on every resource with metadata[key]=operator:value — see Metadata.

Sorting

?sort=attr sorts ascending; ?sort=attr:desc descending; ?sort=last_name,first_name:asc applies one direction to several attributes. GET /schema/{resource} marks which attributes are sortable.

Pagination

Lists are paginated with page (from 1) and page_size (default and maximum 100). Every list response carries navigation links and counts:
Follow links.next until it is null; the links preserve your filters and sort. Ordering is stable — the record id is always applied as a final tiebreaker — so pages never repeat or skip rows for a fixed dataset. Records created or deleted between pages can still shift offset pages, and deep pages get slower on large tables — for syncs and exports use a cursor instead. page is capped at 10,000 — higher values are clamped to the cap and meta.current_page reports the page actually served, not the one requested; walk deeper datasets with cursor pagination instead.

Cursor pagination

Add cursor= (empty) to a list request to walk it by cursor. Each page’s links.next carries an opaque token that encodes where the last row sat in the sort order; the server resumes strictly after it (an index seek, not a skip), so every page costs the same however deep you are, and a record created or deleted elsewhere in the set cannot shift or duplicate what you have already seen. Same filters, sort and page_size apply.
Follow links.next until it is null. Cursors are forward-only, carry no total, and are bound to the query they were issued for — reusing one with different filters, sort or page_size is a 422.

Discovering the model

GET /schema/{resource} is the contract for that resource:
embeds lists the related records every read of the resource carries (a patient’s numbers[], a care plan’s billing), each as a JSON Schema fragment keyed by the attribute name. detail_embeds lists what GET /{resource}/{id} adds on top of the list shape — a care plan’s schedule phases[], billing periods[], services[], products[] and visit tracking — and an embed named in both is the fuller shape a single-record read returns. Either is [] when the resource has nothing to embed. The API Reference is generated from the same source, so both always agree.

Response shape

  • Attributes are snake_case; ids are integers; empty values are null.
  • Every record includes id and a metadata object.
  • Only the attributes listed in GET /schema/{resource} are ever returned — new attributes may be added over time, existing ones are not renamed or removed within the current version.

Deleted records

Records deleted in PracticeHub disappear from their resource. GET /deleted_entities lists what was deleted and when, so a sync can remove its copies — filter with deleted_at=gte:{last sync} (not created_at, which is the deleted record’s own original stamp) and read entity_type / entity_id.

Query cookbook

Every filter, operator and field below is verified against GET /schema/{resource} and the API’s query parser — copy, paste, swap in your own ids and dates. Patients updated since a timestamp — the standard incremental-sync query:
Appointments for a practitioner this week, earliest first:
Unpaid invoices — invoices.state is paid, unpaid or void:
balance (the amount still owed) is filterable and sortable too — balance=gt:0 is the numeric equivalent if you want it alongside other conditions. Payments in a date range, by method:
Patients by partial name or email — like needs an explicit % wildcard (URL-encoded); contains doesn’t:
Records carrying a given metadata key value — any resource, any key you’ve written (see Metadata):
Null / not-null — patients missing an email, and patients with a dob on file:
Combining filters — repeat an attribute to AND several conditions, in for an OR within one attribute: pending or arrived appointments at either of two locations, next month:
Counting without fetching — page_size=1 still runs the full count; read meta.total and ignore data:
A full cursor walk — the robust way to pull an entire resource for a sync or export, immune to rows shifting between pages: