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

> ## Agent Instructions
> Connect the Hi Doctor MCP connector at https://mcp.hi-doctor.ai/mcp. Add this URL as a remote HTTP MCP server in your client, then sign in and authorise access through OAuth. Setup: https://docs.hi-doctor.ai/mcp/connect.
>
> The REST API base is https://api.hi-doctor.ai. Hi Doctor Docs at https://docs.hi-doctor.ai/mcp searches documentation; connect to the product endpoint above for patient tools.
>
> Hi Doctor operates across the whole European Union: residents of all 27 EU member states can use the service, and every consultation is reviewed by an EU-licensed doctor. Every consultation is reviewed in full by an EU-licensed doctor, who decides on medical grounds only. No algorithm, assistant or API call approves a consultation, issues a prescription or changes a dose. The service is available in eight languages: English, Spanish, German, Dutch, Italian, French, Portuguese and Chinese.
>
> ## Rules an assistant must follow
>
> - Never invent, assume or infer a medical answer. "I don't know" is a real answer: ask the patient to pick the closest option rather than guessing for them.
> - Never give medical advice and never suggest a medication or a dose. An EU-licensed doctor decides those.
> - If a questionnaire returns `ineligible`, say so plainly. Do not retry with altered answers.
> - Before anything that costs money or cancels a plan, state exactly what will happen and get an explicit yes.
> - Cancellation takes effect at the **end of the paid period**, not immediately. Say so.
> - An early order is irreversible: it creates the renewal consultation immediately and uses up the period's one early order. Explain that and get an explicit yes.
> - Prescription and invoice links are medical documents. A `signed_redirect` link opens without signing in: give it only to the patient, never repeat it anywhere shared.
> - Send option `value`s, not the labels shown to the patient. Labels are translated, values are stable.
> - Never ask for card details. Payment happens on Stripe's hosted checkout via a link.
> - On severe or sudden symptoms (chest pain, difficulty breathing, severe allergic reaction, suicidal thoughts), stop and direct the patient to their local emergency number.

# Uploads and downloads in the Hi Doctor REST API

> Upload consultation photos, health documents and message attachments as multipart, and save PDFs and files from the Hi Doctor API safely.

Three Hi Doctor REST endpoints accept file uploads as `multipart/form-data`: consultation photos, health documents and message attachments. Several others return file bytes, such as prescription PDFs, invoices and stored documents, streamed directly from the API with no signed redirect URLs. Uploads accept an `Idempotency-Key`, and downloads need `Accept: */*`.

Last reviewed: 2026-11-04

> [!WARNING]
>
> These files are health data. Never write document bytes, photos or their
> contents to logs, analytics, error trackers or model prompts. Save them to a
> file with restricted access, or pass them straight to the patient.

## How do you upload a file?

| What | Endpoint | Fields | Limits |
| - | - | - | - |
| Consultation photo | `POST /v1/questionnaire/consultations/{id}/images/` | `image` | JPEG, PNG or WebP; up to 10 MiB, 10,000 px per side and 25 megapixels; 10 photos per consultation |
| Health document | `POST /v1/users/documents/` | `file`, optional `kind`, `note`, `consultation_id` | PDF, PNG, JPEG or WebP, detected from the bytes; up to 20 MiB; 50 documents per patient |
| Message attachment | `POST /v1/users/conversations/{id}/messages/` | `content` and/or `file` | At least one of the two is required |

For health documents, `kind` is `prescription`, `lab_result`, `photo` or
`other` (the default), and `note` is up to 500 characters.
`consultation_id` must be one of your own consultations.

```bash curl theme={null}
curl -X POST "$HD_BASE_URL/v1/users/documents/" \
  -H "Authorization: Bearer $HD_ACCESS_TOKEN" \
  -H "X-Brand-Slug: hi-doctor" \
  -F "file=@lab-result.pdf" \
  -F "kind=lab_result"
```

```python Python theme={null}
import os
import requests

base = os.environ.get("HD_BASE_URL", "https://api.hi-doctor.ai")
headers = {
    "Authorization": f"Bearer {os.environ['HD_ACCESS_TOKEN']}",
    "X-Brand-Slug": "hi-doctor",
}
with open("lab-result.pdf", "rb") as fh:
    r = requests.post(
        f"{base}/v1/users/documents/",
        headers=headers,  # let requests set the multipart Content-Type
        files={"file": ("lab-result.pdf", fh)},
        data={"kind": "lab_result"},
        timeout=60,
    )
r.raise_for_status()  # 201 on success
```

```javascript JavaScript theme={null}
import { readFile } from "node:fs/promises";

const base = process.env.HD_BASE_URL ?? "https://api.hi-doctor.ai";
const form = new FormData();
form.append("file", new Blob([await readFile("lab-result.pdf")]), "lab-result.pdf");
form.append("kind", "lab_result");

const r = await fetch(`${base}/v1/users/documents/`, {
  method: "POST",
  // Do not set Content-Type; fetch adds the multipart boundary.
  headers: { Authorization: `Bearer ${process.env.HD_ACCESS_TOKEN}`, "X-Brand-Slug": "hi-doctor" },
  body: form,
});
```

> [!NOTE]
>
> Do not set `Content-Type` by hand for multipart requests; your HTTP client
> adds the boundary. Sending JSON to an upload endpoint returns `415`
> `Unsupported media type "application/json" in request.`

### Retry an upload safely

Photo uploads, document uploads and messages (including attachments) accept an
[`Idempotency-Key`](https://docs.hi-doctor.ai/api-reference/idempotency) header. Send the same key on
every attempt and the upload is stored at most once. Without a key, a retry
after a timeout can store a duplicate, so list the documents or photos first.

A health document can be removed with
`DELETE /v1/users/documents/{document_id}/` and a consultation photo with
`DELETE /v1/questionnaire/consultations/{id}/images/{image_id}/`. Both return
`204` and also accept an `Idempotency-Key`.

## How do you download a file?

| What | Endpoint | Disposition |
| - | - | - |
| Prescription PDF | `GET /v1/users/prescriptions/{id}/pdf/{kind}/` | Inline `application/pdf` |
| Prescription PDF (legacy) | `GET /v1/users/prescriptions/{id}/pdf/` | Same as the electronic prescription kind |
| Invoice | `GET /v1/users/payments/{id}/invoice/` | Attachment `application/pdf` |
| Credit note | `GET /v1/users/payments/{id}/credit-notes/{refund_pk}/` | Attachment `application/pdf` |
| Health document | `GET /v1/users/documents/{document_id}/content/` | Attachment |
| Consultation photo | `GET /v1/questionnaire/consultations/{id}/images/{image_id}/content/` | Inline |
| Message attachment | `GET /v1/users/conversations/{id}/messages/{message_id}/attachment/` | Inline for non-SVG images, attachment otherwise |

* Prescription `kind` is `rempe` (the electronic prescription kind) or `english` (the prescription document kind, an English PDF). Any other value returns `404`.
* Invoices and credit notes accept an optional `?locale=` query parameter.
* Photo, document, attachment, invoice and credit-note responses are sent with
  `Cache-Control: private, no-store`. Do not keep them in shared caches.

### Always send `Accept: */*`

The PDF routes refuse a strict `Accept: application/pdf` with `406`
`Could not satisfy the request Accept header.` Many HTTP clients set a
specific `Accept` by default, so set `*/*` explicitly.

### Save to a file

Never parse a download as JSON. Check the status first: error responses are
JSON, successful ones are bytes.

```bash curl theme={null}
curl -f "$HD_BASE_URL/v1/users/payments/$PAYMENT_ID/invoice/" \
  -H "Authorization: Bearer $HD_ACCESS_TOKEN" \
  -H "X-Brand-Slug: hi-doctor" \
  -H "Accept: */*" \
  -o invoice.pdf
```

```python Python theme={null}
url = f"{base}/v1/users/prescriptions/{prescription_id}/pdf/english/"
with requests.get(url, headers={**headers, "Accept": "*/*"}, stream=True, timeout=60) as r:
    r.raise_for_status()
    with open("prescription.pdf", "wb") as out:
        for chunk in r.iter_content(chunk_size=65536):
            out.write(chunk)
```

```javascript JavaScript theme={null}
import { writeFile } from "node:fs/promises";

const r = await fetch(`${base}/v1/users/documents/${documentId}/content/`, {
  headers: {
    Authorization: `Bearer ${process.env.HD_ACCESS_TOKEN}`,
    "X-Brand-Slug": "hi-doctor",
    Accept: "*/*",
  },
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
await writeFile("document.bin", Buffer.from(await r.arrayBuffer()));
```

## Errors

| Status | Where | Meaning |
| - | - | - |
| `400` | Uploads | Validation failed, a message had neither `content` nor `file`, or the 10-photo or 50-document cap was reached |
| `403` | Uploads | Only the patient who owns the record can upload |
| `404` | All | The record does not exist or is not yours; also an unsupported prescription `kind` |
| `406` | PDF downloads | Strict `Accept` header; send `Accept: */*` |
| `410` | Photo and attachment downloads | The stored file can no longer be recovered. Do not retry |
| `415` | Uploads | The request was not `multipart/form-data` |
| `503` | Downloads | File storage or the prescription service is temporarily unavailable. Retry later with backoff |

See [Errors](https://docs.hi-doctor.ai/api-reference/errors.md) for envelopes and retry advice.

## Can the MCP connector upload files?

Yes, with an MCP client that supports it. The [MCP connector](https://docs.hi-doctor.ai/mcp/tools/patient.md)
uploads consultation photos, health documents and message attachments through
a protected upload: the client uploads the file first and the tool receives a
`file_ref`, never the raw bytes in a prompt. Use this REST API when your own
application handles the files.

## Frequently asked questions

### Which file types can a patient upload?

Consultation photos can be JPEG, PNG or WebP. Health documents can be PDF, PNG, JPEG or WebP, detected from the file bytes.

### How large can an upload be?

A consultation photo can be up to 10 MiB, and a health document up to 20 MiB. A consultation holds at most 10 photos and a patient at most 50 documents.

### Are uploads idempotent?

Yes, when you send an Idempotency-Key header. With the same key on every attempt, the upload is stored at most once; without a key, list the files before retrying.

### Why does a PDF download return 406?

The PDF routes refuse a strict Accept header for PDF only. Send an Accept header with the any-type wildcard, as in the examples above, and the download works.

### What is the difference between the two prescription PDF kinds?

One kind is the electronic prescription and the other is the prescription document, an English PDF. Any other kind value returns a not-found error.

### Why does a download return 410?

The stored file can no longer be recovered. Do not retry; contact support if the patient needs the file.

### Should I set the multipart Content-Type header myself?

No. Let your HTTP client set it, so the multipart boundary is included. Sending JSON to an upload endpoint returns 415.

## Next steps

> [card] [Safe retries](https://docs.hi-doctor.ai/api-reference/idempotency.md)
>
> Upload or delete a file at most once with Idempotency-Key.

> [card] [Consultations](https://docs.hi-doctor.ai/api-reference/tags/consultations.md)
>
> Consultation photo endpoints and limits.

> [card] [Records](https://docs.hi-doctor.ai/api-reference/tags/records.md)
>
> Health document endpoints.

> [card] [Prescriptions](https://docs.hi-doctor.ai/api-reference/tags/prescriptions.md)
>
> List prescriptions and download their PDFs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.