> ## Documentation Index
> Fetch the complete documentation index at: https://build.practicehub.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Best practices

> How to build an integration that stays correct and fast under real clinic load — limits, retries, pagination, time, money, data hygiene, credentials

The API is small and regular; most integration bugs are not about the API but about the things around it — clocks, retries, floats, pagination that shifts under you. This page collects the habits that avoid them. Each one is short; the [examples](/examples/overview) show them applied.

## 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-Remaining` are on every response; a `429` carries `Retry-After` (seconds).
* **Retry `429` and `5xx` with backoff; never `4xx`.** Honour `Retry-After` when present, otherwise exponential (1, 2, 4, 8 s) with jitter, capped at 4–5 attempts. A `4xx` means the request is wrong — retrying it is a loop.
* **Retry `POST`s only with an `Idempotency-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. `PATCH`/`DELETE` are 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 `.updated` webhooks 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 follow `links.next` until `null`. 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>`, sort `updated:asc`, walk by cursor, then set the watermark to the time the run *started* (with a few minutes' overlap) — see the [accounting example](/examples/accounting-export).

## Time

* **Two clocks.** `created` and `updated` (and webhook `occurred_at`) are **UTC** timestamps for windowing. Everything the practice *sees* — appointment `start`/`end`, `invoice_date`, `payment_date`, availability slots — is in the **clinic's timezone**, as `YYYY-MM-DD HH:MM:SS` or `YYYY-MM-DD` with no offset. `GET /me` tells 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 read `2026-09-01 09:00:00` as 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`), never `float`. Send them back the same way (`"45.00"`, not `45`).
* **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.** `409` with a `code` (`slot_unavailable`, `appointment_rule`, `group_full`, `billing_rule`…) means the practice's rules said no; the `message` is written to be shown to a person. `422` with `errors` is your input. Neither should be retried.
* **Send what changed.** `PATCH` is 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_items` on invoices, `numbers` on patients: read, edit, send back with ids; omit an id to add, leave one out to remove.
* **Availability then book, never book blind.** `/availability` returns exactly what is bookable; copy the slot into `POST /appointments`. `allow_clash` exists 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 `*.deleted` event or a `404` on 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-Remaining` trends 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 /me` at startup: right account, right scopes, timezone and currency stored.
* [ ] Retries: `429`/`5xx` only, backoff, `Idempotency-Key` on creates.
* [ ] Walks use `?cursor=`; incremental runs use `updated` watermarks with overlap.
* [ ] Times are clinic-local strings; money is decimal; `updated`/`created` only for windowing.
* [ ] Webhook receiver verifies signatures, acks fast, de-duplicates on event `id`.
* [ ] Errors: branch on `code`, show `message`, log `X-Request-Id`.
* [ ] Tested against a real account with the practice's own rules (a slot clash, a refused edit, a void).
