> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hi-doctor.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Every error envelope the Hi-Doctor API returns, what each status code means, and whether to retry, re-authenticate or fix the input.

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.

<AccordionGroup>
  <Accordion title="Field validation — the most common by far">
    An object keyed by **field name**, each value an **array** of messages.
    There is no `detail` and no `code` here.

    ```json theme={null}
    { "weight_kg": ["This field is required."] }
    ```

    Several fields fail at once, so expect more than one key:

    ```json theme={null}
    {
      "treatment_id": ["This field is required."],
      "medication_name": ["This field is required."],
      "description": ["This field is required."],
      "severity": ["This field is required."],
      "onset_date": ["This field is required."]
    }
    ```

    Errors that belong to the request as a whole rather than to one field
    arrive under `non_field_errors`:

    ```json theme={null}
    { "non_field_errors": ["At least one item is required"] }
    ```

    For a list field, the value is an **array positionally matching what you
    sent** — index 0 is the first item you submitted:

    ```json theme={null}
    { "items": [{ "dosage": ["This field is required."] }] }
    ```
  </Accordion>

  <Accordion title="detail — permissions, routing and content negotiation">
    ```json theme={null}
    { "detail": "Authentication credentials were not provided." }
    ```

    ```json theme={null}
    { "detail": "No Consultation matches the given query." }
    ```

    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.
  </Accordion>

  <Accordion title="detail with code — token failures">
    Token problems add a machine-readable `code`, and sometimes a `messages`
    array with the specific reason.

    ```json theme={null}
    {
      "detail": "Given token not valid for any token type",
      "code": "token_not_valid",
      "messages": [
        { "token_class": "AccessToken", "token_type": "access", "message": "Token is invalid" }
      ]
    }
    ```
  </Accordion>

  <Accordion title="error with code — sign-in">
    The auth endpoints use `error`, not `detail`.

    ```json theme={null}
    { "error": "Invalid credentials", "code": "invalid_credentials" }
    ```

    Some carry only the message:

    ```json theme={null}
    { "error": "Google token is required" }
    ```
  </Accordion>

  <Accordion title="error with error_name — recoverable business rules">
    These name a condition your client is expected to handle, and often carry
    the data needed to recover from it.

    ```json theme={null}
    { "error": "Registration session expired or not found", "error_name": "VERIFY_EMAIL_SESSION_EXPIRED" }
    ```

    ```json theme={null}
    { "error_name": "PROFILE_INCOMPLETE", "missing_fields": ["date_of_birth", "country_of_residence"] }
    ```
  </Accordion>
</AccordionGroup>

<Note>
  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.
</Note>

## Status codes

| Status | Meaning                                                            | What the client should do                  |
| ------ | ------------------------------------------------------------------ | ------------------------------------------ |
| `400`  | Failed validation, or a business rule refused the request          | **Fix the input.** Never retry unchanged   |
| `401`  | Missing, expired or malformed token — or wrong sign-in credentials | **Re-authenticate.** Refresh, then sign in |
| `403`  | Authenticated, but not permitted — e.g. a disabled account         | **Stop.** Retrying cannot succeed          |
| `404`  | No such record, **or it is not yours**                             | **Stop.** Do not probe other ids           |
| `405`  | Wrong HTTP method for that path                                    | **Fix the request**                        |
| `406`  | Content negotiation refused your `Accept` header                   | **Send `Accept: */*`**                     |
| `409`  | Conflicts with current state                                       | **Re-read the resource** and decide        |
| `429`  | Rate limited                                                       | **Back off** — see below                   |

### 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`**:

  ```json theme={null}
  { "detail": "Unknown questionnaire category: weight-lost" }
  ```

  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 itself** — `code: "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".

<Warning>
  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.
</Warning>

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

<Warning>
  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.
</Warning>

### 406 — the PDF trap

The invoice and prescription PDF endpoints reject a *specific* binary `Accept`:

```json theme={null}
{ "detail": "Could not satisfy the request Accept header." }
```

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:

```json theme={null}
{ "code": "already_active" }
```

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.

<Warning>
  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.
</Warning>

## 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](/mcp/overview) applies its own limits, separately from this
API, keyed on the access token — see [Security](/mcp/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.

<Warning>
  Never present a clinical refusal to a patient as a technical error, and never
  retry it with modified answers on their behalf. See [Safety](/essentials/safety).
</Warning>

## Related

* [Authentication](/api-reference/authentication) — tokens, refresh and sign-in failures
* [API reference](/api-reference/introduction) — conventions and the typical flow
* [Sign up a new patient](/api-reference/signup) — registration and verification failures
