Skip to main content
Errors are JSON. The awkward part is that the API does not use one error envelope — it uses five, depending on which layer refused the request. A client that reads only detail, or only error, will silently lose the message on roughly half of all failures.

The five envelopes

Read whichever key is present rather than assuming one. Branch on code or error_name when they appear — those are stable identifiers. The prose in detail and error is written for humans and may be reworded.
An object keyed by field name, each value an array of messages. There is no detail and no code here.
Several fields fail at once, so expect more than one key:
Errors that belong to the request as a whole rather than to one field arrive under non_field_errors:
For a list field, the value is an array positionally matching what you sent — index 0 is the first item you submitted:
The model name in that message changes with the resource (No WeightEntry matches the given query.), so match on the status code, never on the string.
Token problems add a machine-readable code, and sometimes a messages array with the specific reason.
The auth endpoints use error, not detail.
Some carry only the message:
These name a condition your client is expected to handle, and often carry the data needed to recover from it.
A robust client reads, in order: detail, then error, then the first message of the first field in the object. Treat code and error_name as the thing you branch on, and the prose as the thing you log.

Status codes

400 — fix the input

Read the field map, correct the named fields, and resubmit. Two cases deserve special handling because they are not really input errors:
  • PROFILE_INCOMPLETE at checkout returns missing_fields. PATCH /v1/users/profile/me/ with exactly those fields, then retry the checkout.
  • An unknown questionnaire slug is a 400, not a 404:
    Fetch /v1/users/questionnaires/categories/ for the valid slugs rather than guessing.

401 — re-authenticate

Distinguish the two causes, because the recovery differs:
  • On a normal endpoint — the access token is missing, expired or malformed. Exchange the refresh token at /v1/users/token/refresh/ and replay the request once. If the refresh also returns 401 ({ "detail": "Token is invalid", "code": "token_not_valid" }), the session is over; the patient must sign in again.
  • On sign-in itselfcode: "invalid_credentials". This is deliberately non-specific: it does not reveal whether the address exists, and it is also what an unverified account gets. Never render it as “no such account”.
Never retry a 401 with the same token. It cannot start working, and on sign-in a retry loop is what turns a typo into a lockout.

404 — it may exist, just not for you

Every endpoint is scoped to the authenticated account. A record belonging to someone else is indistinguishable from one that does not exist — both return 404. That is intentional: a 403 would confirm the record exists.
A 404 is never a reason to retry with a different id. There is no patient identifier to pass on any endpoint in this reference, and enumerating ids is treated as abuse.

406 — the PDF trap

The invoice and prescription PDF endpoints reject a specific binary Accept:
Sending Accept: application/pdf — the obvious thing to send for a PDF — fails. Send Accept: */* instead. Many HTTP clients set a specific Accept for you, so this usually has to be overridden explicitly.

409 — already in that state

Returned when the request conflicts with the resource’s current state, such as reactivating a subscription that was never scheduled for cancellation:
Treat this as the outcome you wanted already holds, not as a failure to retry.

429 — back off

If you receive a 429, stop and wait. Honour the Retry-After header if one is present, otherwise retry with exponential backoff and jitter. Never retry in a tight loop.
On email verification, repeated attempts extend the lockout rather than shortening it, and a throttled registration retry deliberately does not invalidate the code already sitting in the patient’s inbox. Tell them to look for the earlier email instead of promising a new one.

Rate limits

Hi-Doctor does not publish numeric rate limits for this API, and this page deliberately does not invent any. What is worth knowing:
  • The application layer does not apply a global throttle to these endpoints, so a limit you encounter in production comes from the edge in front of the API, not from the endpoint itself. Its configuration is not public and can change without a change to this reference.
  • Because of that, you cannot infer a budget from testing. A sequence that succeeds today may be throttled tomorrow.
  • Write your client so that a 429 on any call is handled, rather than assuming only the auth endpoints are limited.
The MCP connector applies its own limits, separately from this API, keyed on the access token — see Security.

5xx

A 5xx is a fault on Hi-Doctor’s side, not a problem with your request. Retry idempotent reads with backoff. Do not blindly retry a write: a checkout, a questionnaire submission or a message may have been recorded before the error was returned. Re-read the resource to find out before sending it again.

Clinical outcomes are not errors

An ineligible questionnaire result is a 200-level clinical decision, not a failure to route around. Contraindications, interactions, age, BMI and country are enforced at submission, and resubmitting altered answers to get a different outcome is a misuse of the service.
Never present a clinical refusal to a patient as a technical error, and never retry it with modified answers on their behalf. See Safety.