Credentials
- One key per integration. Requests, rate limits and audit trails are per key, so “the booking widget” and “the nightly export” should never share one. Name keys after the integration.
- Read-only unless you write. A read-only key cannot write whatever it tries (
403 key_read_only) — the cheapest security control you have. - Server-side only. Never ship a key in a browser, mobile app or public repository.
phk_keys are recognised by secret scanners; if one leaks, rotate it (the old secret keeps working for 24 h while you switch). - Expiry + rotation for anything long-lived; rotate keys with a calendar reminder, not after an incident.
- Connectors are people. An OAuth connector acts as the team member who approved it, with their PracticeHub role — for anything automated use an API key, not someone’s connector.
Rate limits and retries
- 600 requests per minute per key.
X-RateLimit-Limit/X-RateLimit-Remainingare on every response; a429carriesRetry-After(seconds). - Retry
429and5xxwith backoff; never4xx. HonourRetry-Afterwhen present, otherwise exponential (1, 2, 4, 8 s) with jitter, capped at 4–5 attempts. A4xxmeans the request is wrong — retrying it is a loop. - Retry
POSTs only with anIdempotency-Key. A timeout on a create is the one case where you don’t know whether it happened; the key makes the retry return the original result.PUT/PATCH/DELETEare safe to repeat as they are. - Batch by fetching lists, not records. 100 patients is one request with
page_size=100, not 100 requests. Filters (updated=gte:…,in:) exist so you can ask precisely. - Cache reference data (locations, appointment types, practitioners, payment methods) for minutes and refresh on their
.updatedwebhooks rather than reading them on every call.
Pagination
- Browsing / random access:
page+page_size(max 100). Ordering is stable (id is always the last tiebreaker). - Any walk that writes, syncs or exports:
?cursor=and followlinks.nextuntilnull. Constant cost per page, unaffected by rows appearing or disappearing while you walk, no risk of skipping. Cursors are bound to their filters/sort/page_size — build the first URL fresh each run. - Incremental runs: filter
updated=gte:<watermark>, sortupdated:asc, walk by cursor, then set the watermark to the time the run started (with a few minutes’ overlap) — see the accounting example.
Time
- Two clocks.
createdandupdated(and webhookoccurred_at) are UTC timestamps for windowing. Everything the practice sees — appointmentstart/end,invoice_date,payment_date, availability slots — is in the clinic’s timezone, asYYYY-MM-DD HH:MM:SSorYYYY-MM-DDwith no offset.GET /metells you the timezone once; store it. - Send clinic-local times to book. Do not convert to UTC and do not append
Z— the API will read2026-09-01 09:00:00as 9 am at the clinic. - DST is the clinic’s problem, not yours — as long as you keep clinic-local times as strings and only convert at display time using the account timezone.
Money
- Amounts are fixed 2-dp strings (
"45.00"). Parse them into a decimal type (Decimal,BigDecimal,bcmath,Money), neverfloat. Send them back the same way ("45.00", not45). - Currency comes from
/me, once per account. There is one currency per account. - Balances are computed (
invoices.balance,patient_balance,third_party_balance) — read them; don’t sum line items yourself and expect equality to the penny after tax and rounding.
Writes
- Expect refusals and show them.
409with acode(slot_unavailable,appointment_rule,group_full,billing_rule…) means the practice’s rules said no; themessageis written to be shown to a person.422witherrorsis your input. Neither should be retried. - Send what changed.
PATCHis partial and last-write-wins; sending the whole record back re-applies stale fields over changes staff made in between. - Reconcile child lists by id —
line_itemson invoices,numberson patients: read, edit, send back with ids; omit an id to add, leave one out to remove. - Availability then book, never book blind.
/availabilityreturns exactly what is bookable; copy the slot intoPOST /appointments.allow_clashexists for deliberate double-booking by staff tools, not for public flows. - Let the app do the side effects. Confirmations, reminders, balances, audit entries all happen because you went through the API — don’t replicate them.
Data hygiene
- Store ids, not copies. Keep the PracticeHub id (and put your id in
metadata); fetch details when you need them. Fewer copies of clinical/personal data on your side is less to protect and keeps you current. - Webhooks (coming soon) are thin on purpose — the event tells you what changed, the API tells you to what. Don’t try to reconstruct records from event streams.
- Log the
X-Request-Id, never the request body, when you log API calls; quote it to support. Your own request log is at Developers → API Logs. - Delete means gone. A
*.deletedevent or a404on something you had means soft-deleted or voided — remove or archive it on your side; don’t keep serving it. - Metadata is shared per record across integrations on the account — prefix keys with your app name (
crm_id,crm_synced_at) and keep to a handful.
Resilience
- Design every handler to be re-run. Backfills, nightly runs and webhook handlers should all be safe to execute twice: upsert by PracticeHub id, de-duplicate events by
id, idempotency keys on creates. - Acknowledge webhooks in milliseconds, do work in a queue — a slow endpoint gets retried and eventually disabled.
- Watch the headers. If
X-RateLimit-Remainingtrends to zero in normal operation, you are polling something that should be a webhook or a filter. - Version-tolerant parsing. New attributes and event types appear without notice (additive contract); ignore what you don’t know, never fail on an unexpected key.
Before you go live
- Key is server-side, named, read-only where possible, with an expiry.
-
GET /meat startup: right account, right scopes, timezone and currency stored. - Retries:
429/5xxonly, backoff,Idempotency-Keyon creates. - Walks use
?cursor=; incremental runs useupdatedwatermarks with overlap. - Times are clinic-local strings; money is decimal;
updated/createdonly for windowing. - Webhook receiver verifies signatures, acks fast, de-duplicates on event
id. - Errors: branch on
code, showmessage, logX-Request-Id. - Tested against a real account with the practice’s own rules (a slot clash, a refused edit, a void).