Skip to main content
Every date and time value says which clock it is on by its shape. A UTC stamp ends in Z (2026-09-01T14:31:00Z). Clinic wall-clock time has no zone at all (2026-09-01T09:00:00). A calendar day is a plain date (2026-09-01). GET /schema/{resource} says the same per attribute: a timezone of utc or account.

Formats

The API uses exactly three date/time shapes, all ISO 8601: That is the same convention as the PracticeHub web app’s own data: a Z means UTC, no Z means the account’s clock. On input send the same shape you read. A UTC stamp may also carry an offset (2026-09-01T15:31:00+01:00) and is converted. Clinic wall time may carry no zone: starts_at: "2026-09-01T09:00:00Z" is a 422, because the value would then mean a different moment from the one the clinic reads. Seconds are required; fractional seconds are accepted and dropped. GET /schema/{resource} names the rule per attribute (date_format:Y-m-d\TH:i:s\Z, date_format:Y-m-d\TH:i:s or date_format:Y-m-d), and a value in the wrong shape is a 422 naming the field.

Whose clock

Every domain date/time — the ones that mean something in the clinic’s day, like appointments.starts_at/ends_at, payment_date and invoice_date — is the clinic’s local wall-clock time, not UTC and not the caller’s timezone. 2026-09-01T09:00:00 on appointments.starts_at means 9am as the clinic’s front desk would say it, however many timezones away your server sits. There’s no offset in the value because none is needed: the account has exactly one timezone, and every domain date/time on it is already in that timezone. The server-managed record stamps are different: created_at, updated_at and the other *_at stamps are UTC and end in Z. They tell you when a row changed, not when something happened in the clinic’s day — use them for incremental sync cursors, where a single monotonic clock is exactly what you want, and use the domain fields for anything a person would read as a date. Call GET /me to find out which zone the account is on — account.timezone, an IANA name (Europe/London, America/New_York, …):
Do this once when you connect an integration and cache it — an account’s timezone and currency don’t change often, but they’re not something to hardcode per clinic. If your system works in UTC or another timezone internally, convert at the boundary: localise before you send a write, and interpret every zone-less date/time you read as being in account.timezone, not your server’s zone.
Filters follow the same rule. starts_at=gte:2026-09-01T00:00:00 in Querying means midnight in the clinic’s timezone; updated_at=gte:2026-09-01T00:00:00Z is UTC midnight.

Currency

account.currency from GET /me is the account’s single operating currency (an ISO 4217 code, GBP in the example above) — every amount on every resource (payments.amount, invoices.total / balance / subtotal, line_items.price, …) is in that currency. There’s no per-record or per-line currency field to check: PracticeHub accounts don’t mix currencies. Amounts are decimal strings or numbers with at most two decimal places — "45.00", not 45 or "45.0000". A third decimal place is rejected as invalid input rather than silently rounded, so a value like 19.999 never becomes 20.00 or 19.99 without you knowing. An amount outside the range is 422 amount_out_of_range; a line_items.quantity outside 1–2147483647 (a whole number) is 422 quantity_out_of_range.