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

# Files and patient history

> Uploading documents against a patient, and writing to their activity timeline

Two resources round out a patient's record beyond their clinical and financial data: `files` — documents, images and reports attached to them — and `patient_logs` — the chronological activity timeline the Activity Log tab shows. Both are written through the exact services the app's own Documents, Images and Activity Log tabs use, so a file or entry created through the API behaves identically to one a team member added by hand.

## Files

### Uploading

`POST /files` is `multipart/form-data`, not JSON — the one write on the API that isn't:

```bash theme={null}
curl -X POST $BASE/files \
  -H "Authorization: Bearer $KEY" \
  -F "patient_id=340" \
  -F "file_type=generic" \
  -F "description=Referral letter" \
  -F "patient_viewable=false" \
  -F "file=@referral-letter.pdf"
```

| Field | Notes |
| - | - |
| `patient_id` | Required |
| `file_type` | Required — see [Types](#types) below |
| `file` | Required. Max **50 MB**. Extension must be one of `jpg`, `jpeg`, `png`, `mp3`, `pdf`, `txt`, `csv`, `doc`, `docx`, `xls`, `xlsx` — checked against both the filename's extension and the file's actual sniffed content, so a renamed executable or HTML file is refused even with an allowed extension |
| `description` | Optional, ≤ 255 chars |
| `patient_viewable` | Optional boolean — whether the patient can see this file in the patient app |
| `patient_log_id` | Required only when `file_type` is `patient_log` — see below |

### Types

| Type | Meaning |
| - | - |
| `generic` | An ordinary document — the default for anything that isn't one of the others |
| `custom_content` | Content specifically surfaced to the patient. **Re-typing** an existing file to `custom_content` via `PATCH` always sets `patient_viewable` to `true`, even if you didn't send it — matching the app's Documents tab, because an invisible "custom content" file is a contradiction. On upload, `patient_viewable` still defaults to `false` unless you set it explicitly |
| `diagnostic_image` | Scans, X-rays and similar clinical images |
| `patient_profile` | The patient's profile photo. Must be an image (`jpg`, `jpeg` or `png` on upload; a re-type checks the *stored* file's mime type instead, so a `.jpg` misnamed at upload still qualifies once its real content is detected). **A patient has exactly one profile photo** — uploading a new one, or re-typing an existing file to `patient_profile`, demotes every other live `patient_profile` file of that patient to `generic` and clears the cached photo the app was serving, so the change shows immediately |
| `patient_log` | An attachment on an Activity Log entry — see [Attachments on log entries](#attachments-on-log-entries). Only settable at upload, and locked forever once set |

### Updating

`PATCH /files/{id}` changes `description`, `patient_viewable` and `file_type` — nothing else; the file's content, its patient and its `patient_log` link are fixed at upload and refused (not silently ignored) if you send them again.

```bash theme={null}
curl -X PATCH $BASE/files/9021 \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "file_type": "diagnostic_image", "patient_viewable": true }'
```

`file_type` can move between `generic`, `custom_content`, `diagnostic_image` and `patient_profile` — **never** to or from `patient_log`. A `patient_log` attachment refuses any re-type with `422 patient_log_attachment_locked`: an attachment's identity is the log entry it documents, and that can't change after the fact.

### Reading and downloading

`GET /files` and `GET /files/{id}` return the file's metadata — `file_name`, `mime_type`, `size`, `file_type`, `description`, `patient_viewable`, and which patient it belongs to. **The current resource set does not return a URL to fetch the binary itself** — there is no download endpoint yet. If your integration needs the file content today, that's a gap to plan around rather than something to poll for; check [the changelog](/changelog) before building around an assumption here.

### Deletion

`DELETE /files/{id}` soft-deletes the record. The underlying stored object is only removed from storage once no other live file row still points at the same storage key — a safety check rather than something an integration needs to think about under normal use, where each upload gets its own key.

## Attachments on log entries

To attach a file to an Activity Log entry, upload it as an ordinary file with `file_type=patient_log` and `patient_log_id` set to the entry:

```bash theme={null}
curl -X POST $BASE/files \
  -H "Authorization: Bearer $KEY" \
  -F "patient_id=340" -F "file_type=patient_log" -F "patient_log_id=8842" \
  -F "file=@intake-form.pdf"
```

The log entry must already exist and belong to the same patient (`422` otherwise). Deleting the **log entry** takes its `patient_log` attachments with it — they're soft-deleted and unlinked in the same transaction, the same as the Activity Log tab's delete does. Deleting the **file** on its own just removes that one attachment; the entry stays.

## Patient logs

`patient_logs` is the patient's activity timeline — the same feed the Activity Log tab renders, filterable by type:

```bash theme={null}
curl -X POST $BASE/patient_logs \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "patient_id": 340,
    "type": "note",
    "data": "Discussed home exercise adherence — patient reports doing sets 3x/week.",
    "sub_type": "clinical"
  }'
```

| Field | Notes |
| - | - |
| `patient_id` | Required |
| `type` | Required — one of the seven values below |
| `data` | Required — the entry's text content |
| `sub_type` | Optional, ≤ 255 chars — a free-text refinement of `type` you choose (not validated against a list) |
| `source_entity` | Optional, ≤ 255 chars — a free-text label for what generated the entry, when `type` alone isn't specific enough |

### Types

| `type` | Used for |
| - | - |
| `note` | Free-text clinical or administrative notes — the Activity Log tab's default |
| `comms` | A record of communication with the patient (calls, messages, letters) |
| `financial` | Billing-related activity |
| `appt` | Appointment-related activity |
| `membership` | Membership / care-plan-related activity |
| `system` | System-generated entries |
| `historical` | Not shown as a filter in the app's own Activity Log tab — reserved for narrative history brought over from a previous system. See [Data migration](/guides/data-migration#7-history-entries) |

An entry is stamped with **who wrote it** automatically: entries created through the API carry `source: api` and are attributed to your integration in the app's views, exactly like every other write — you never set `source` yourself (sending it is refused).

### Updating and deleting

The activity log is also PracticeHub's audit trail, so the API can change or delete **only the entries it created** (`source: api`). Entries written by the team or by PracticeHub itself — appointment, payment and communication history — answer `422 log_entry_locked`.

`PATCH /patient_logs/{id}` changes `type`, `data`, `sub_type` and `source_entity`. An entry can't move to another patient (`patient_id` is refused on update), and its `source` is permanent.

`DELETE /patient_logs/{id}` soft-deletes the entry **and** takes its `patient_log`-type file attachments with it, in the same transaction — nothing is left dangling.

## Tags

`patient_log_tags` label individual entries — a lightweight way to mark entries for later filtering beyond what `type` and `sub_type` give you:

```bash theme={null}
curl -X POST $BASE/patient_log_tags \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "patient_log_id": 8842, "tag_name": "follow-up-needed" }'
```

`patient_log_id` must be a real entry on the account (`422 patient_log_not_found` otherwise); `tag_name` is required, ≤ 64 chars. `PATCH` can rename a tag (`tag_name`) or move it to a different entry (`patient_log_id`, re-checked the same way); `DELETE` removes it. There's no uniqueness constraint enforced on the pair — tagging the same entry with the same name twice creates two rows.

See [Writing](/guides/writing#files) for the full field-by-field contract and error codes, and [Data migration](/guides/data-migration#6-documents) for bulk-importing documents and history alongside billing and appointment records.


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