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, likeappointments.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, …):
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.