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

# Patient, document, note

> Create a patient, upload a document and generate a clinical note from it

Use this when you have a patient's paperwork (an intake form, referral letter
or prior records) and want HealOS to write a note from it, with no recording.

| | |
| - | - |
| **Scopes** | `patients:write`, `documents:read`, `documents:write`, `notes:read`, `notes:write` |
| **Cost** | Step 4 runs AI note generation. Steps 1 to 3 and 5 are free. |
| **Outside systems** | None. Nothing is sent to an EHR or payer. |
| **Time** | Extraction 10 to 30 seconds per PDF; the note in about a minute. |

> [!TIP]
>
> Try it in a [test organization](https://docs.healos.ai/test-mode.md) first, with a document that
> holds only made-up details.

```bash theme={null}
export HEALOS_API_KEY="hlsk_..."
export HEALOS="https://api.healos.ai/ext-api/v1"
```

**1. Create the patient**

```bash theme={null}
curl -X POST "$HEALOS/patients" \
  -H "X-API-Key: $HEALOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "first_name": "Testy", "last_name": "Recipe", "date_of_birth": "1970-01-01", "external_id": "test-pt-001" }'
```

**Expect** `201`. Save `data.id` as `PATIENT_ID`. If the patient already
exists, you get `409` with `existing.id`; use that.

**2. Upload the document**

Send the file as `multipart/form-data`. PDF, JPEG, PNG and DOCX up to 10 MB
are accepted, and the content must match the file extension.

```bash theme={null}
curl -X POST "$HEALOS/patients/$PATIENT_ID/documents" \
  -H "X-API-Key: $HEALOS_API_KEY" \
  -F "file=@test-intake-form.pdf" \
  -F "external_id=test-doc-001"
```

**Expect** `201`, `data.status: "pending"` and a `message` that ends in
"text extraction started". If `message` doesn't mention extraction,
start it yourself with
[`POST .../documents/{documentId}/extract`](https://docs.healos.ai/api-reference/documents/extract-or-retry-document-text-extraction).

**3. Wait for text extraction**

Register a `DOCUMENT_STATUS_CHANGED` [webhook](https://docs.healos.ai/webhooks.md), or poll every
few seconds:

```bash theme={null}
curl "$HEALOS/patients/$PATIENT_ID/documents?external_id=test-doc-001" \
  -H "X-API-Key: $HEALOS_API_KEY"
```

**Expect** `data[0].status` to move from `pending` to `processing` to
`completed`, with `has_extracted_text: true`. On `failed`, read
`extraction_error`.

**4. Generate the note**

```bash theme={null}
curl -X POST "$HEALOS/patients/$PATIENT_ID/notes/from-documents" \
  -H "X-API-Key: $HEALOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "balanced" }'
```

**Expect** `202` with `data.session_id`. Save it as `SESSION_ID`.

> [!WARNING]
>
> Each call creates a new session and note. Don't retry after a `202`;
> check the session in the next step.

**5. Read the note**

Wait for a `NOTE_CREATED` [webhook](https://docs.healos.ai/webhooks.md), or poll every 10 seconds:

```bash theme={null}
curl "$HEALOS/sessions/$SESSION_ID/notes" -H "X-API-Key: $HEALOS_API_KEY"
```

**Expect** `200`. `data.notes` is empty until the note is written, then
holds it with its `content` and codes.

## The same flow in Python

The steps above as one script, using
[requests](https://pypi.org/project/requests/) (`pip install requests`). It
reads the key from `HEALOS_API_KEY` and expects a test file holding only
made-up details next to it.

```python document_to_note.py theme={null}
import os
import time

import requests

BASE = "https://api.healos.ai/ext-api/v1"
http = requests.Session()
http.headers["X-API-Key"] = os.environ["HEALOS_API_KEY"]


def check(res, allowed=()):
    if not res.ok and res.status_code not in allowed:
        raise RuntimeError(f"{res.request.method} {res.url} -> {res.status_code}: {res.json().get('error')}")
    return res.json()


# 1. Create the patient (a 409 means it already exists: reuse it).
res = http.post(f"{BASE}/patients", json={
    "first_name": "Testy",
    "last_name": "Recipe",
    "date_of_birth": "1970-01-01",
    "external_id": "test-pt-001",
}, timeout=30)
body = check(res, allowed=(409,))
patient_id = body["existing"]["id"] if res.status_code == 409 else body["data"]["id"]

# 2. Upload the document (a 409 means this external_id was already uploaded).
with open("test-intake-form.pdf", "rb") as f:
    res = http.post(
        f"{BASE}/patients/{patient_id}/documents",
        files={"file": ("test-intake-form.pdf", f, "application/pdf")},
        data={"external_id": "test-doc-001"},
        timeout=60,
    )
check(res, allowed=(409,))
if res.status_code == 201 and "text extraction started" not in res.json().get("message", ""):
    document_id = res.json()["data"]["id"]
    check(http.post(f"{BASE}/patients/{patient_id}/documents/{document_id}/extract", timeout=60))

# 3. Wait for text extraction.
for _ in range(60):
    docs = check(http.get(
        f"{BASE}/patients/{patient_id}/documents",
        params={"external_id": "test-doc-001"},
        timeout=30,
    ))["data"]
    if docs[0]["status"] == "failed":
        raise RuntimeError(f"Extraction failed: {docs[0].get('extraction_error')}")
    if docs[0]["status"] == "completed":
        break
    time.sleep(5)
else:
    raise RuntimeError("Extraction is still running; check the document later")

# 4. Generate the note. Sent once: each call creates a new session and note.
queued = check(http.post(
    f"{BASE}/patients/{patient_id}/notes/from-documents",
    json={"model": "balanced"},
    timeout=60,
))
session_id = queued["data"]["session_id"]

# 5. Read the note.
notes = []
for _ in range(30):
    time.sleep(10)
    notes = check(http.get(f"{BASE}/sessions/{session_id}/notes", timeout=30))["data"]["notes"]
    if notes:
        break
print(notes[0]["content"] if notes else f"Note not ready yet; check session {session_id} later")
```

In production, replace the two polling loops with the
`DOCUMENT_STATUS_CHANGED` and `NOTE_CREATED` [webhooks](https://docs.healos.ai/webhooks.md).

## Expected outcomes

| Case | Expected result | What to do |
| - | - | - |
| Normal PDF with text | Document `completed`, then `202`, then one note | Nothing |
| Scanned PDF or photo | Usually `completed` (text is read from the image) | If `failed`, upload a clearer copy |
| File content doesn't match its extension | Upload `400` | Send the right extension |
| File over 10 MB | Upload `413` | Split or compress it |
| Same `external_id` uploaded twice | Upload `409` with `existing` | Use the existing document |
| Note requested before any document is `completed` | `422`, no session created | Wait for extraction, then retry |
| `template_id` not in your organization | `404` | Pick an id from [`GET /templates`](https://docs.healos.ai/templates) |
| Key without `notes:write` | `403` `Insufficient scope. Required: notes:write` | Add the scope to the key |

## Related

* [Clinical notes guide](https://docs.healos.ai/clinical-notes.md#generate-a-note-from-documents)
* [Webhooks: document upload flow](https://docs.healos.ai/webhooks.md#document-upload-flow)
* [Generate a note from documents](https://docs.healos.ai/api-reference/notes/generate-a-note-from-the-patients-documents.md) (reference)


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