Skip to main content
Four moves cover almost every “why did this happen” question: read the code, find the request in Developers → API Logs, reproduce it with curl -i and read the headers, and check it against the gotchas below before you conclude something is broken.

1. Read the code

Every error is { message, code } (plus errors for a 422). message is written for a human and can be reworded without notice — branch your code, and your own eyeballing, on code. The full list of codes, grouped by status, is the error code catalogue; it’s generated from what the API actually emits, so if you see a code that isn’t there, it’s worth a support ticket on its own.
group_full is a 409 — a business rule, not a bug. Errors has the full retry taxonomy: 422 means fix the input, 409 (other than an idempotency replay) means the record’s state has to change first, 429/5xx are safe to retry with backoff.

2. Find it in API Logs

Developers → API Logs is a report over every call your account’s keys and connectors have made — a summary (success rate, error breakdown, p50/p95 latency), a timeline chart (hourly buckets up to 48 hours of range, daily beyond that, stacked by success / 4xx / 429 / 5xx), breakdowns by credential and by endpoint, a top error-codes table, and the log itself: one row per request with its credential, method + path, status/error code, duration, and Request ID.
  • Narrow before you scroll. Filter by time range (1h/24h/7d/30d/90d, or a custom clinic-local date range), method, resource (entity), status class or exact status, error_code, a minimum duration, or a path substring. Rows are pruned after 90 days — a custom range further back than that is clamped, not silently empty.
  • Have a Request ID already? Paste it (or just the start of it) into the Request ID filter — it matches as a prefix, so a truncated id copied out of a client log still finds the row.
  • Query parameter values aren’t logged, only their names. A filter’s ?patient_id=eq:340 shows up as query_keys: patient_id — the log tells you that you filtered on patient_id, not what value you sent, because filter values can carry patient data. Cross-check the value against what your integration actually sent.
  • A 422’s row already tells you the field. The logged error_message for a validation failure isn’t just “Validation failed.” — the first entry from the response’s errors object (field name + rule text) is appended, so the row is self-explanatory without opening the detail sheet.
  • Anonymous failures only show up if a credential was presented. A request with no bearer token at all that gets a 401/429 isn’t logged (nothing to attribute it to); one with a bad or expired token is.
  • Idempotent-Replayed is a column, not a guess. If a row is marked replayed, that request never re-ran your write — it’s a repeat of a key you (or a retrying HTTP client) already used.
Click into a row for the detail sheet: it repeats the Request ID with a copy button and the note “Quote this when contacting support” — that’s the id to hand PracticeHub, and it’s the same one on the response’s X-Request-Id header (see below).

3. Reproduce with curl -i and read the headers

Run the failing call again with -i so you see the response headers, not just the body:
  • X-Request-Id is the one that matters for support and for API Logs — it’s what’s stored against the row, what the Request ID filter searches, and what the detail sheet tells you to quote. It’s generated fresh per request; you can’t set your own.
  • X-Trace-Id is a second, separate id on every response (API and non-API alike) — it’s echoed back if you send your own X-Trace-Id request header, or generated if you don’t. It is not stored in the API request log and doesn’t appear in API Logs, so it isn’t what you search the report with. It exists for PracticeHub engineering to correlate a request against internal application logs; mention it if support asks for it specifically, otherwise lead with X-Request-Id.
  • X-RateLimit-Limit / X-RateLimit-Remaining are on every response, success or failure — worth glancing at even when the call worked, if you’re chasing an intermittent 429.
  • On a POST you retried with the same Idempotency-Key, look for Idempotent-Replayed: true — it means you got the original response back, not a re-run of the write. If you expected a second record and got this instead, that’s the explanation, not a bug.
  • On a 429, read Retry-After — the number of seconds until the bucket has room. It’s per account, per surface (120/minute for REST, shared by every API key on the account — AI connectors have their own separate MCP bucket); a script and a manual test running at the same time spend the same allowance, so each exhausts it faster than either alone.

4. Common gotchas

Check these before assuming the API is wrong — each one has bitten a real integration and produces an error that looks like something else.

When you contact support

Have these ready — all but the last come straight out of curl -i and the API Logs detail sheet:
  • The X-Request-Id from the response (or from the log row’s detail sheet) — this is what support pulls the request by.
  • The timestamp, with its timezone — clinic-local (as shown in API Logs) or UTC; either is fine as long as you say which.
  • The endpoint — method and path (POST /v3/api/appointments, not just “the appointments endpoint”).
  • The code from the response body, not just the status — 409 group_full and 409 appointment_rule need different fixes, and only the code tells them apart.
  • Whether an Idempotency-Key was involved, and whether you reused one — a replayed request behaving “wrong” is usually the original request’s outcome, not a new bug, and support needs to know you reused a key to rule that in or out.