Getting connected
Where do I get an API key, and why doesn't my old one work?
Where do I get an API key, and why doesn't my old one work?
phk_…. (An older-style key from an earlier PracticeHub API will not work here.) See Authentication.What is my base URL? The docs say {clinic}.
What is my base URL? The docs say {clinic}.
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.Which account/timezone/currency am I actually talking to?
Which account/timezone/currency am I actually talking to?
GET /me — returns the integration, its scopes and the account (id, name, timezone, currency). Make it your first call.Is there a sandbox?
Is there a sandbox?
Can I connect my own agent or another MCP client to the MCP server?
Can I connect my own agent or another MCP client to the MCP server?
GET /schema contract.Is the MCP server read-only? When do writes arrive?
Is the MCP server read-only? When do writes arrive?
Can my AI coding tool read these docs?
Can my AI coding tool read these docs?
https://build.practicehub.io/mcp, with a skill file at /skill.md and a page index at /llms.txt. Snippets for Claude, Claude Code, ChatGPT, Cursor, VS Code and Windsurf are on Docs MCP. That server reads documentation only; it cannot see a clinic’s data.Errors
401 unauthenticated
401 unauthenticated
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.403 key_read_only
403 key_read_only
403 permission_denied / connector_not_permitted
403 permission_denied / connector_not_permitted
404 on a record I know exists
404 on a record I know exists
404 means you cannot have it.405 unsupported_operation
405 unsupported_operation
practitioners, modalities, resources, care_plans, numbers, addresses, …), some can’t be deleted (payment_methods, referral_sources, appointment_types, locations, billable_items). GET /schema/{resource} lists what is allowed. See Resources.409 — what's the difference between appointment_rule, group_full and billing_rule?
409 — what's the difference between appointment_rule, group_full and billing_rule?
group_full: the group session has no places left — re-query availability. (A plain booking on a taken time is not refused — POST /appointments books what you send, like a staff click; check availability first.) 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.422 with errors — but I sent exactly what the docs show
422 with errors — but I sent exactly what the docs show
errors: it names the attribute. Common ones: starts_at not YYYY-MM-DDTHH: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.429 rate_limited
429 rate_limited
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.Where do I see what my integration sent?
Where do I see what my integration sent?
X-Request-Id (no bodies). Quote the X-Request-Id when you contact support.Data & behaviour
Why are appointment times different from what I expect by an hour or more?
Why are appointment times different from what I expect by an hour or more?
starts_at/ends_at, invoice_date, payment_date and availability slots are clinic-local with no offset; created_at/updated_at are UTC. Don’t convert the former; use /me for the timezone. See Best practices → Time.Why are amounts strings?
Why are amounts strings?
"45.00" in, "45.00" out — parse into a decimal type.Why did my POST /appointments succeed but the appointment isn't there — or is there twice?
Why did my POST /appointments succeed but the appointment isn't there — or is there twice?
Idempotency-Key on creates: a retry with the same key returns the original response instead of booking again. See Writing.I updated a patient's phone number and their other numbers disappeared.
I updated a patient's phone number and their other numbers disappeared.
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.Why don't voided payments show up in my export?
Why don't voided payments show up in my export?
deleted_entities tombstone feed. (Billing webhook events are not in the current rollout — patient and appointment events first; see Webhooks.) Voided invoices stay visible with state: "void".Can I read clinical notes / send SMS / charge a card?
Can I read clinical notes / send SMS / charge a card?
/clinical_notes (see Clinical notes). SMS and card payments: no — messaging is the practice’s, and a payment a provider collects (card, Direct Debit) cannot be recorded through the API because no money is collected (card_payment_not_allowed). See Resources for what is and isn’t available.Availability shows nothing / 422 not_online_bookable / availability_not_configured
Availability shows nothing / 422 not_online_bookable / availability_not_configured
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.Do webhooks tell me what changed?
Do webhooks tell me what changed?
My webhook endpoint got disabled.
My webhook endpoint got disabled.
Can I filter by my own ids?
Can I filter by my own ids?
metadata and filter ?metadata[your_key]=eq:value (or =null for “not yet synced”). See Metadata.X-Request-Id from the response.