> ## 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.

# FAQ & troubleshooting

> The questions integrators ask most — and the first thing to check for each

## Getting connected

<AccordionGroup>
  <Accordion title="Where do I get an API key, and why doesn't my old one work?" defaultOpen>
    **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](/authentication).
  </Accordion>

  <Accordion title="What is my base URL? The docs say {clinic}.">
    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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="Is there a sandbox?">
    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.
  </Accordion>
</AccordionGroup>

## Errors

<AccordionGroup>
  <Accordion title="401 unauthenticated">
    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.
  </Accordion>

  <Accordion title="403 key_read_only">
    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).
  </Accordion>

  <Accordion title="403 permission_denied / connector_not_permitted">
    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.
  </Accordion>

  <Accordion title="404 on a record I know exists">
    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.
  </Accordion>

  <Accordion title="405 unsupported_operation">
    That resource doesn't support that operation: some are read-only (`practitioners`, `locations`, `appointment_types`, `modalities`, `numbers`, `addresses`, …), some can't be deleted (`payment_methods`, `referral_sources`). `GET /schema/{resource}` lists what is allowed. See [Resources](/resources).
  </Accordion>

  <Accordion title="409 — what's the difference between appointment_rule, slot_unavailable and billing_rule?">
    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](/guides/booking). `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.
  </Accordion>

  <Accordion title="422 with errors — but I sent exactly what the docs show">
    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.
  </Accordion>

  <Accordion title="429 rate_limited">
    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](/guides/best-practices).
  </Accordion>

  <Accordion title="Where do I see what my integration sent?">
    **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.
  </Accordion>
</AccordionGroup>

## Data & behaviour

<AccordionGroup>
  <Accordion title="Why are appointment times different from what I expect by an hour or more?">
    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](/guides/best-practices#time).
  </Accordion>

  <Accordion title="Why are amounts strings?">
    So no client rounds them as floats. `"45.00"` in, `"45.00"` out — parse into a decimal type.
  </Accordion>

  <Accordion title="Why did my POST /appointments succeed but the appointment isn't there — or is there twice?">
    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](/guides/writing#retrying-safely-idempotency-keys).
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="Why don't voided payments show up in my export?">
    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](/examples/accounting-export).
  </Accordion>

  <Accordion title="Can I read clinical notes / send SMS / charge a card?">
    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](/resources) for what is and isn't in v1.
  </Accordion>

  <Accordion title="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](/guides/booking).
  </Accordion>

  <Accordion title="Do webhooks tell me what changed?">
    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](/examples/webhook-receiver).
  </Accordion>

  <Accordion title="My webhook endpoint got disabled.">
    (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.
  </Accordion>

  <Accordion title="Can I filter by my own ids?">
    Yes — put them in `metadata` and filter `?metadata[your_key]=eq:value` (or `=null` for "not yet synced"). See [Metadata](/guides/metadata).
  </Accordion>
</AccordionGroup>

## Legacy API

<AccordionGroup>
  <Accordion title="Why is the legacy /api being retired?">
    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](/why-the-new-api#why-retire-the-legacy-api-rather-than-extend-it).
  </Accordion>

  <Accordion title="When does it switch off?">
    Legacy responses carry `Deprecation` now and will carry `Sunset` with the date once it is fixed, plus a `Link` to the [migration guide](/guides/migrating-from-legacy). Both APIs run side by side until then.
  </Accordion>

  <Accordion title="Do my legacy filters and field names still work?">
    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](/guides/migrating-from-legacy).
  </Accordion>
</AccordionGroup>

<Note>
  Not answered here? Email <a href="mailto:support@practicehub.io">[support@practicehub.io](mailto:support@practicehub.io)</a> with the clinic subdomain, the endpoint, and the `X-Request-Id` from the response.
</Note>
