Skip to main content

Getting connected

Where do I get an API key, and why doesn't my old one work?

Developers → API Keys → API key in PracticeHub (a team member with the developer permission). Legacy x-practicehub-key keys are for the legacy /api only; new API keys are Bearer tokens that look like phk_…. See Authentication.
Every clinic account has its own host: https://<your-clinic>.<region>.practicehub.io/v3/api — exactly the host the practice signs in on; the region host is the one in the practice’s login URL — neptune (London), mercury (Frankfurt), vulcan (Ohio), jupiter (Singapore) or apollo (Sydney). The docs use {clinic}.{region} / your-clinic.your-region because there is no single URL — which is also why the “Try it” button is off on the reference pages: copy the request into your terminal with your host instead.
GET /me — returns the integration, its scopes and the account (id, name, timezone, currency). Make it your first call.
No. Test against a spare “test patient” on the practice’s own account, using a read-only key wherever the integration does not need to write, and agree any test writes with the practice first. Every request is visible on Developers → API Logs, so test traffic is easy to review.

Errors

No Authorization: Bearer header, a typo in the key, an expired or revoked key — or the right key on the wrong clinic host (keys are per account). Check the host first.
The key was created Read only and you sent a write. Create a Read & write key for this integration (don’t widen a key that other systems already use).
Only for OAuth connectors (Claude, ChatGPT…): the team member’s PracticeHub role doesn’t allow that read/write/delete, or no longer has Connect AI apps. The message names the permission; an account owner changes it under Settings → Security → Roles. API keys are never affected by roles.
Wrong clinic host; the record is soft-deleted or voided (it no longer comes back); or — for connectors — it is outside the person’s own-patients / own-calendar scope. There is no “hidden but present” state: a 404 means you cannot have it.
That resource doesn’t support that operation: some are read-only (practitioners, locations, numbers, addresses, patient_payment_methods, …), some can’t be deleted (appointment_types, referral_sources, …). GET /schema/{resource} lists what is allowed. See Resources.
All three mean the practice’s own rules refused it, same as a staff member would be refused in the app. slot_unavailable/group_full: the diary isn’t free — re-query availability. appointment_rule: the appointment’s state forbids the change (already checked out, cancelled, missed) — the message says which. billing_rule: an invoice/payment rule (already void, over-allocation, last line item…). Show the message; don’t retry.
Read errors: it names the attribute. Common ones: start not YYYY-MM-DD HH:MM:SS; an unknown attribute or filter name (they are refused, not ignored); a numbers.*.number that isn’t a real phone number for the iso2 given, or a number without a type; amount with more than 2 decimals; a metadata key over 40 chars.
600 requests per minute per key. Wait Retry-After seconds. If you hit it in normal operation you are polling something — use filters, page_size=100, cursors and webhooks. See Best practices.
Developers → API Logs in PracticeHub: every request by key, with method, path, status, latency and X-Request-Id (no bodies). Quote the X-Request-Id when you contact support.

Data & behaviour

Appointment start/end, invoice_date, payment_date and availability slots are clinic-local with no offset; created/updated are UTC. Don’t convert the former; use /me for the timezone. See Best practices → Time.
So no client rounds them as floats. "45.00" in, "45.00" out — parse into a decimal type.
A timeout after PracticeHub wrote the record. Always send an Idempotency-Key on creates: a retry with the same key returns the original response instead of booking again. See Writing.
numbers is reconciled by id: send the stored numbers with their id (you get them on every read) to keep/update them, add new ones without an id, and leave out the ones to remove. A list with no ids at all is treated as “replace everything”. Same model as line_items on invoices.
Voiding soft-deletes a payment (and the allocations an invoice void releases): they stop being returned by list and get endpoints, and fire payments.deleted / payment_allocations.deleted webhooks. Voided invoices stay visible with state: "void". See the accounting example.
No — deliberately out of scope for now (clinical notes are not exposed; messaging is the practice’s; card payments cannot be recorded through the API because no card is charged, card_payment_not_allowed). See Resources for what is and isn’t in v1.
visibility=online (the default) only offers what the practice has enabled for online booking — location, appointment type and practitioner. visibility=all shows the staff view. availability_not_configured means there is no rota or service for that location + type at all. See Booking.
Webhooks are coming soon (not yet switched on). When they ship: no — payloads carry the event type and record id only (no personal or clinical data leaves via webhooks). Fetch the record. Events aren’t ordered; the record you fetch is always current. See the webhook receiver.
(Applies once webhooks ship.) It timed out or returned non-2xx repeatedly. Acknowledge within a couple of seconds and do the work afterwards; then re-enable and replay missed messages from Developers → Webhooks.
Yes — put them in metadata and filter ?metadata[your_key]=eq:value (or =null for “not yet synced”). See Metadata.

Legacy API

Short version: it predates typed contracts, webhooks, idempotency, AI assistants and the app’s service layer, and keeping two APIs would mean building everything twice. The long version is on Why the new API.
Legacy responses carry Deprecation now and will carry Sunset with the date once it is fixed, plus a Link to the migration guide. Both APIs run side by side until then.
Yes — resource names, attribute names and the ?field=op:value filter language are unchanged; two tightenings (unknown attribute → 422; no operator → eq). Update auth, base URL, response parsing and the update verb — usually an afternoon. See Migrating.
Not answered here? Email support@practicehub.io with the clinic subdomain, the endpoint, and the X-Request-Id from the response.