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

# Med lists from PDFs

> Submit clinical PDFs (referrals, H&Ps, discharge summaries, progress notes) and get back a deduplicated medication list.

`POST /v1/med-lists/from-documents` accepts one or more clinical PDFs for a
single patient and returns the same kind of deduplicated medication list you'd
get from `/v1/medications/infer` — but driven by the document text instead of
structured FHIR.

Use it when your input is what the patient or referring clinic actually sends:
a faxed referral packet, an Epic-rendered discharge summary, a printed
After-Visit Summary, or a stack of progress notes. We pull the medication
mentions from each document, reconcile them across the packet, and return one
entry per drug with the page / text-snippet evidence it was inferred
from.

<CardGroup cols={2}>
  <Card title="Try it in the playground" icon="flask" href="https://medlistiq.com/dashboard/playground">
    Upload your own clinical PDFs — or run a pre-loaded sample — and see the
    deduplicated med list right in your browser. Pick "PDFs" under "Source
    data". Uploaded files go straight to the API and are never stored.
  </Card>

  <Card title="API key" icon="key" href="/authentication">
    Same Bearer key that powers `/v1/medications/infer`. No separate setup.
  </Card>
</CardGroup>

## Quickstart

<Steps>
  <Step title="Get an API key">
    Same flow as the FHIR endpoint — see [Authentication](/authentication).
  </Step>

  <Step title="POST your PDFs as multipart/form-data">
    Each file goes under the `files` form field. Repeat `files=@…` for every
    PDF in the packet.

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST https://api.medlistiq.com/v1/med-lists/from-documents \
        -H 'Authorization: Bearer ml_YOUR_KEY' \
        -F 'files=@discharge_summary.pdf' \
        -F 'files=@h_and_p.pdf' \
        -F 'files=@referral_note.pdf'
      ```

      ```python Python theme={null}
      import httpx

      with open("discharge_summary.pdf", "rb") as a, \
           open("h_and_p.pdf", "rb") as b, \
           open("referral_note.pdf", "rb") as c:
          response = httpx.post(
              "https://api.medlistiq.com/v1/med-lists/from-documents",
              headers={"Authorization": "Bearer ml_YOUR_KEY"},
              files=[
                  ("files", ("discharge_summary.pdf", a, "application/pdf")),
                  ("files", ("h_and_p.pdf", b, "application/pdf")),
                  ("files", ("referral_note.pdf", c, "application/pdf")),
              ],
              timeout=60.0,
          )
      print(response.json())
      ```

      ```typescript TypeScript theme={null}
      const form = new FormData();
      for (const path of ["discharge_summary.pdf", "h_and_p.pdf", "referral_note.pdf"]) {
        const file = await Bun.file(path).arrayBuffer(); // or fs.readFile in Node
        form.append("files", new Blob([file], { type: "application/pdf" }), path);
      }

      const response = await fetch(
        "https://api.medlistiq.com/v1/med-lists/from-documents",
        {
          method: "POST",
          headers: { Authorization: "Bearer ml_YOUR_KEY" },
          body: form,
        },
      );
      const data = await response.json();
      console.log(data);
      ```
    </CodeGroup>

    <Tip>
      Don't set `Content-Type: multipart/form-data` manually — let your HTTP
      client compute the boundary. Setting the header by hand drops the
      boundary and the request fails validation.
    </Tip>
  </Step>

  <Step title="Read the response">
    ```json theme={null}
    {
      "medications": [
        {
          "drug_name": "penicillin V Potassium 250 MG Oral Tablet",
          "rxnorm_code": "834061",
          "rxnorm_tty": "SCD",
          "ingredients": [
            {
              "rxnorm_code": "7984",
              "name": "penicillin V",
              "rxnorm_tty": "IN",
              "strength_quantity": "250",
              "strength_unit": "mg"
            }
          ],
          "dose_quantity": "250",
          "dose_unit": "mg",
          "route": "PO",
          "route_snomed_code": "26643006",
          "route_ncit_code": "C38288",
          "dose_form": "oral tablet",
          "frequency": null,
          "sources": [
            {
              "document_name": "h_and_p.pdf",
              "page": 1,
              "evidence_text": "Pen-Vee K 250 MG Oral Tablet"
            },
            {
              "document_name": "discharge_summary.pdf",
              "page": 1,
              "evidence_text": "Penicillin V Potassium 500 MG Oral Tablet"
            },
            {
              "document_name": "referral_note.pdf",
              "page": 1,
              "evidence_text": "STOPPED: Penicillin V Potassium 250 MG Oral Tablet"
            }
          ]
        }
      ]
    }
    ```

    The H\&P referenced the brand `Pen-Vee K` and the others used the generic
    `Penicillin V Potassium` — all three collapse into one entry, with each
    mention preserved as a `sources[]` item.
  </Step>
</Steps>

## How dedup works

A medication mentioned in three documents becomes **one** entry in
`medications` with **three** items in `sources[]`. Cross-document reconciliation
matches by canonical drug name (brand → generic where applicable), so:

* `Lipitor` in one doc and `Atorvastatin` in another → one `Atorvastatin` entry
* The same med in 5 of 8 progress notes → one entry, 5 sources
* Differing doses across docs → the reconciler picks the one from the most
  authoritative section (Discharge > Active/Current > Home/Outpatient > MAR > others)

## Response shape

The body contains only `medications`. Cross-cutting metadata lives in
**response headers**:

| Header                      | Meaning                                        |
| --------------------------- | ---------------------------------------------- |
| `x-request-id`              | UUID for support tickets / log lookup          |
| `x-document-count`          | Number of PDFs in the request                  |
| `x-total-page-count`        | Total pages across all PDFs after parsing      |
| `x-output-medication-count` | `len(medications)` — same as the body field    |
| `x-processing-time-ms`      | End-to-end latency on our side                 |
| `x-ruleset-version`         | Versioned tag of the extraction logic that ran |

Each entry in `medications[]` is an `ExtractedMedication`:

| Field               | Type           | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `drug_name`         | string         | Human-readable name for the medication. For most resolved drugs (SCD / SBD), this is the catalog's canonical name — already including ingredient, strength, and dose form (e.g. `"penicillin V Potassium 250 MG Oral Tablet"`). For branded packs (BPCK), we simplify the verbose `{...} Pack [Brand]` canonical to `"<Brand> Pack"` (e.g. `"Trinessa 28 Day Pack"`) since the full enumeration isn't suitable for display. For ingredient-only resolutions, this is the canonical ingredient name (`"Metformin"`). |
| `rxnorm_code`       | string \| null | Most-specific RxCUI we resolved. SCD / SBD / GPCK / BPCK (ingredient + strength + dose form) when we had enough fields; falls back to less-specific TTYs otherwise. `null` only when nothing matched. The RxNorm system URI is always `http://www.nlm.nih.gov/research/umls/rxnorm` — we don't repeat it on every entry.                                                                                                                                                                                            |
| `rxnorm_tty`        | string \| null | Term type of `rxnorm_code`. SCD / SBD / GPCK / BPCK = fully-specified drug. SCDC / SBDC = ingredient + strength, no form. IN / PIN / MIN / BN = ingredient-only — treat as a fallback.                                                                                                                                                                                                                                                                                                                              |
| `ingredients[]`     | array          | Ingredients of this medication, each with its RxNorm code, term type, and per-ingredient strength when known. One entry for single-ingredient drugs, multiple for combinations (e.g. amlodipine / valsartan). Use these for drug-interaction checks and class-level allergy matching where the SCD-level top-level `rxnorm_code` is usually too specific.                                                                                                                                                           |
| `dose_quantity`     | string \| null | For single-ingredient drugs, the numeric portion of the parsed dose, kept as a string so range values like `"1-2"` and decimals like `"12.5"` survive. For combo products with a countable form (`"oral tablet"`, `"transdermal patch"`, `"vaginal system"`, etc.) this is always `"1"` since the patient takes one unit. `null` for combos with non-countable forms or when no dose was detected.                                                                                                                  |
| `dose_unit`         | string \| null | Unit portion of the dose. For single-ingredient: `"mg"`, `"mcg"`, `"ml"`, etc. For combos with countable forms: the patient-friendly form name (`"tablet"`, `"patch"`, `"vaginal system"`). Lowercase.                                                                                                                                                                                                                                                                                                              |
| `route`             | string \| null | Normalized route abbreviation (`PO`, `IV`, `IM`, `SC`, `INH`, `TOP`, `VAG`, `PR`, `TD`, `OPH`, `OTIC`, `NAS`).                                                                                                                                                                                                                                                                                                                                                                                                      |
| `route_snomed_code` | string \| null | SNOMED CT code for the route (e.g. `"26643006"` for oral, `"16857009"` for vaginal). Implicit system is `http://snomed.info/sct`. `null` when `route` is null or doesn't match the \~30 common routes we have in our route table.                                                                                                                                                                                                                                                                                   |
| `route_ncit_code`   | string \| null | NCI Thesaurus code for the route (e.g. `"C38288"` for oral, `"C38313"` for vaginal). Implicit system is `http://ncithesaurus.nci.nih.gov`. Pair with `route_snomed_code` — both populate together.                                                                                                                                                                                                                                                                                                                  |
| `dose_form`         | string \| null | Dose form phrase (`"oral tablet"`, `"vaginal system"`, `"transdermal patch"`, …). Lowercase.                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `frequency`         | string \| null | Normalized frequency (`BID`, `TID`, `QHS`, `PRN`, `Q6H`, …).                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `sources[]`         | array          | One item per detected mention. See below.                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

Each `ingredients[]` item:

| Field               | Type           | Notes                                                                                                                                                                              |
| ------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rxnorm_code`       | string         | Ingredient RxCUI.                                                                                                                                                                  |
| `name`              | string         | Canonical ingredient name (e.g. `"atorvastatin"`).                                                                                                                                 |
| `rxnorm_tty`        | string         | RxNorm term type: `"IN"`, `"PIN"` (precise — salt forms), or `"MIN"` (multi-ingredient grouping).                                                                                  |
| `strength_quantity` | string \| null | Numeric strength of THIS ingredient within the parent medication, as published by RxNorm. `"800"` for the 800 mg sulfamethoxazole component of Bactrim. Pair with `strength_unit`. |
| `strength_unit`     | string \| null | Unit for `strength_quantity` (`"mg"`, `"mcg"`, `"mg/hr"` for transdermal patches and vaginal systems). Lowercase.                                                                  |

Each `sources[]` item:

| Field           | Type   | Notes                                               |
| --------------- | ------ | --------------------------------------------------- |
| `document_name` | string | Filename you submitted (`discharge_summary.pdf`).   |
| `page`          | int    | 1-indexed page within that PDF.                     |
| `evidence_text` | string | The raw line of text where the mention was matched. |

Order of `medications[]`: by section recency of each med's canonical mention
(Discharge > Active/Current > …), then alphabetically by `drug_name`.

## More worked examples

The single-ingredient case above (Penicillin V) covers the basic shape.
Two more shapes worth seeing — combo products and packs.

### Combo product (Bactrim)

Mentions of `"Bactrim"` across documents (sulfamethoxazole + trimethoprim)
merge into one entry whose ingredients carry per-strength data
straight from RxNorm:

```json theme={null}
{
  "drug_name": "sulfamethoxazole 800 MG / trimethoprim 160 MG Oral Tablet",
  "rxnorm_code": "198335",
  "rxnorm_tty": "SCD",
  "ingredients": [
    {
      "rxnorm_code": "10180", "name": "Sulfamethoxazole", "rxnorm_tty": "IN",
      "strength_quantity": "800", "strength_unit": "mg"
    },
    {
      "rxnorm_code": "10829", "name": "Trimethoprim", "rxnorm_tty": "IN",
      "strength_quantity": "160", "strength_unit": "mg"
    }
  ],
  "dose_quantity": "1",
  "dose_unit": "tablet",
  "route": "PO",
  "route_snomed_code": "26643006",
  "route_ncit_code": "C38288",
  "dose_form": "oral tablet",
  "frequency": "BID",
  "sources": [
    { "document_name": "h_and_p.pdf", "page": 1, "evidence_text": "Bactrim 800/160 mg PO BID" },
    { "document_name": "discharge_summary.pdf", "page": 1, "evidence_text": "sulfamethoxazole 800 mg / trimethoprim 160 mg oral tablet" }
  ]
}
```

Key behaviors:

* `dose_quantity` / `dose_unit` are `"1"` / `"tablet"` because Bactrim's
  dose form is countable — the patient takes one tablet at a time. The
  per-ingredient strengths sit on `ingredients[]`.
* Both ingredients surface with their RxNorm IN codes — that's what
  you'd key off for drug-interaction or class-level allergy checks.

### Branded pack (Trinessa-style BPCK)

A 28-day contraceptive pack with 4 different pills across the cycle
is one medication (one BPCK), not four:

```json theme={null}
{
  "drug_name": "Trinessa 28 Day Pack",
  "rxnorm_code": "751905",
  "rxnorm_tty": "BPCK",
  "ingredients": [
    {
      "rxnorm_code": "4124", "name": "Ethinyl Estradiol", "rxnorm_tty": "IN",
      "strength_quantity": "0.035", "strength_unit": "mg"
    },
    {
      "rxnorm_code": "31994", "name": "Norgestimate", "rxnorm_tty": "IN",
      "strength_quantity": "0.18", "strength_unit": "mg"
    }
  ],
  "dose_quantity": null,
  "dose_unit": null,
  "route": "PO",
  "route_snomed_code": "26643006",
  "route_ncit_code": "C38288",
  "dose_form": "oral tablet",
  "frequency": null,
  "sources": [
    { "document_name": "h_and_p.pdf", "page": 1, "evidence_text": "{7 (ethinyl estradiol 0.035 MG / norgestimate 0.18 MG Oral Tablet) / ... } Pack [Trinessa 28 Day]" }
  ]
}
```

Key behaviors:

* `drug_name` is the patient-recognizable short form, not the verbose
  RxNorm canonical that enumerates every pill. RxNorm's canonical
  `{7 (...) / 7 (...) / 7 (...) / 7 (inert ingredients ...) } Pack [Trinessa 28 Day]`
  is unusable as a display string; we extract the brand annotation.
* `dose_quantity` / `dose_unit` are `null`. A pack contains different
  pills on different days, so there's no single "one thing per dose"
  summary.
* `ingredients[]` walks through the pack's component pills to surface
  every active ingredient. Strengths reflect what's in each active pill.
* `rxnorm_tty: BPCK` is the signal that this entry represents a pack —
  treat it differently from an SCD if your downstream logic cares.

For a deeper walkthrough of every field and how to interpret them
clinically, see [Understanding the PDF output](/guides/understanding-pdf-output).

## Limits

| Limit                   | Value  | What happens past it    |
| ----------------------- | ------ | ----------------------- |
| Files per request       | 15     | 422 validation error    |
| Bytes per file          | 50 MB  | 422                     |
| Total bytes per request | 150 MB | 422                     |
| Total pages per request | 200    | 422 (during processing) |

Larger packets should be split into multiple calls today. Server-side batching
for very large packets is on the roadmap.

## Errors

Same error envelope and retry semantics as the rest of `/v1/*` — see
[Errors](/resources/errors). Endpoint-specific 422 details:

| Detail                                                                | Cause                                                           |
| --------------------------------------------------------------------- | --------------------------------------------------------------- |
| `at least one file is required`                                       | Empty multipart body — no `files` field at all.                 |
| `every file must have a filename`                                     | A `files` part was uploaded with no filename.                   |
| `too many files: max 15 per request (received N)`                     | More than 15 PDFs.                                              |
| `file 'X' must be application/pdf (got image/png)`                    | Wrong `Content-Type` on a `files` part.                         |
| `file 'X' does not look like a PDF (missing %PDF- header)`            | Bytes don't start with `%PDF-`. Catches renamed `.txt`/`.docx`. |
| `file 'X' is N bytes; max per file is 52428800 bytes`                 | Single file over 50 MB.                                         |
| `total upload size exceeds 157286400 bytes across all files`          | Combined upload over 150 MB.                                    |
| `payload exceeds 200-page synchronous limit; async mode coming later` | Total page count too large.                                     |

`503` here generally means the upstream OCR service was briefly unavailable
or has not been configured. Retry with backoff.

## Differences from `/v1/medications/infer`

|                              | `/v1/medications/infer`                                        | `/v1/med-lists/from-documents`                                                                                         |
| ---------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Input                        | FHIR resources (JSON body)                                     | PDF files (multipart)                                                                                                  |
| Status                       | `active` / `completed` / `stopped` / `cancelled` / `unknown`   | Not surfaced yet — status inference for document-derived meds is in progress; the field will appear in a later release |
| Confidence score             | Per-medication float                                           | Not yet — confidence work in progress                                                                                  |
| Provenance                   | `provenance[med_id]` keyed by med ID, with rule-trace evidence | `sources[]` per medication, with document + page + text-snippet evidence                                               |
| Verbosity / format selectors | `verbosity` × `format`                                         | None — single response shape                                                                                           |
| Latency                      | \~200 ms typical                                               | Several seconds (page-bound)                                                                                           |
| Rate limit cost              | 1 unit                                                         | Higher per-page; see plan limits                                                                                       |

Both endpoints share authentication, organization scoping, and rate-limit
counters. You can build a single integration that uses whichever shape your
upstream system happens to produce.
