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

# How FHIR Extraction works

> Learn the rules FHIR Extraction follows when it turns a note into FHIR resources, so you can read every response correctly

[FHIR basics](https://docs.corti.ai/fhir/fhir-basics.md) explains the FHIR format. This page explains the rules the endpoint follows when it fills that format. It follows five rules:

1. [Each fact becomes one resource.](#each-fact-becomes-one-resource)
2. [Codes are positive.](#codes-are-positive) Negation, history, and uncertainty go into a status or value field.
3. [Codes come from a fixed scope.](#codes-come-from-a-fixed-scope) A mention with no fitting code produces no resource.
4. [Values come from the note.](#values-come-from-the-note) The endpoint fills only what the note states.
5. [Every value points back to the note.](#every-value-points-back-to-the-note) Mentions show the source text, and alternatives show other codes that could fit.

The code scope, the filters, and the Bundle structure are enforced in code. Positive codes and values taken only from the note depend on how a language model reads the note, so an occasional response can break them. [Reduce extraction errors](https://docs.corti.ai/fhir/reduce-errors.md) shows how to catch these cases.

## Each fact becomes one resource

The endpoint splits a note into single facts, and creates one resource for each:

| Note says | Resources |
| - | - |
| "BP 150/90" | Two Observations: one systolic, one diastolic |
| "Na 139, K 4.1" | Two Observations: one per analyte |
| "Denies fever or chills" | Two Conditions: Fever and Chill |
| "Mother has diabetes and hypertension" | Two FamilyMemberHistory resources: one per condition |

The kind of fact decides the resource group:

| Fact | Resource group |
| - | - |
| A diagnosis, problem, symptom, or sign of the patient | [Condition](https://docs.corti.ai/fhir/resource-groups/condition.md) |
| An allergy or intolerance | [AllergyIntolerance](https://docs.corti.ai/fhir/resource-groups/allergy-intolerance.md) |
| A condition of a relative | [FamilyMemberHistory](https://docs.corti.ai/fhir/resource-groups/family-member-history.md) |
| Something done to or for the patient | [Procedure](https://docs.corti.ai/fhir/resource-groups/procedure.md) |
| A measurement, or a finding someone saw, heard, or felt | [Observation](https://docs.corti.ai/fhir/resource-groups/observation.md) |

A procedure and its result are two facts. "Chest X-ray shows consolidation" gives a Procedure for the X-ray and an imaging Observation for the consolidation.

Every response also holds one [Patient and one Encounter](https://docs.corti.ai/fhir/resource-groups/patient-and-encounter.md), even when the note says nothing about them.

## Codes are positive

SNOMED CT has codes that build a negation or a time into the code itself, such as `86699002` Afebrile or `399211009` History of myocardial infarction. The endpoint is built to avoid these codes. It uses the code for the thing itself, and puts the negation, the time, or the uncertainty in a separate field:

| Note says | Code the endpoint does **not** use | What the endpoint returns |
| - | - | - |
| "Afebrile" | `86699002` Afebrile | Condition `386661006` Fever, with `verificationStatus: refuted` |
| "PMH: MI in 2015" | `399211009` History of myocardial infarction | Condition `22298006` Myocardial infarction, with `clinicalStatus: resolved` and `onsetDateTime: "2015"` |
| "NKDA" | `409137002` No known drug allergy | AllergyIntolerance `416098002` Allergy to drug, with `verificationStatus: refuted` |
| "No murmurs" | | Observation `88610006` Heart murmur, with the value `2667000` Absent |
| "Colonoscopy not done" | | Procedure `73761001` Colonoscopy, with `status: not-done` |

Each resource group carries this meaning in a different field:

| Resource group | Field to check | Values that change the meaning |
| - | - | - |
| Condition | `verificationStatus` | `refuted`: the patient does not have it. `provisional`, `differential`, `unconfirmed`: suspected, not confirmed. |
| Condition | `clinicalStatus` | `inactive`, `resolved`, `remission`: the patient had it, but not now. |
| AllergyIntolerance | `verificationStatus`, `clinicalStatus` | `refuted`: someone checked and found no allergy. `resolved`: the allergy is gone. |
| Observation (finding) | `valueCodeableConcept` | `2667000` Absent: the clinician looked and did not find it. |
| Procedure | `status` | `not-done`: the procedure did not happen. `preparation`: it is planned, but has not started. `stopped`: it started, but ended before completion, so part of it was performed. |
| FamilyMemberHistory | none | The endpoint skips negative family history. "No family history of heart disease" produces no resource. |

The result is one code per concept, however the note phrases it. To find every patient with a fever history, you search for one code, `386661006` Fever, and then read the status fields. You do not also need to search for Afebrile, History of fever, or No fever.

> [!WARNING]
>
> Always read the status or value field before you use a `code`. A Condition with code Fever and `verificationStatus: refuted` means the patient does **not** have a fever. If you count codes without checking the status, you count refuted and resolved conditions as current ones.

## Codes come from a fixed scope

For each coded field, the endpoint only searches a fixed part of a coding system. The scope keeps each code the right kind of concept: a Condition `code` is always a disorder, finding, or event, never a procedure.

| Field | Coding system | Built-in scope |
| - | - | - |
| Condition `code` | SNOMED CT | Semantic tag `disorder`, `finding`, or `event` |
| AllergyIntolerance `code` | SNOMED CT | Semantic tag `finding`, such as Allergy to penicillin |
| FamilyMemberHistory `condition[0].code` | SNOMED CT | Semantic tag `disorder` |
| Procedure `code` | SNOMED CT | Semantic tag `procedure` |
| Observation `code`, for measurements | LOINC | Terms with status `ACTIVE` |
| Observation `code`, for findings | SNOMED CT | `404684003` Clinical finding and its descendants |
| Observation `valueCodeableConcept`, for imaging findings | SNOMED CT | Semantic tag `qualifier value`, such as Enlarged |
| `bodySite`, on every group that has one | SNOMED CT | `442083009` Anatomical or acquired body structure and its descendants |

Each resource group page lists its full scope, including fixed lists such as severities and categories, under "Codes the endpoint can assign".

Two consequences follow from the scope:

* **No fitting code, no resource.** When nothing in the scope fits a mention, the endpoint leaves the resource out rather than assign a code that does not fit. "Father gets chest pain" produces no FamilyMemberHistory, because chest pain is a finding, not a disorder.
* **Filters only narrow the scope.** A [filter](https://docs.corti.ai/fhir/filtering.md) can remove codes from the built-in scope, but never add codes outside it.

## Values come from the note

The endpoint fills a field only when the note states the value. It is built not to calculate, guess, or fill in a value from context, apart from the few fixed defaults listed below.

| The endpoint does not | Example | What you get instead |
| - | - | - |
| Calculate dates from relative expressions | "Asthma for two years" | `onsetString: "two years"`, not a date |
| Calculate a date of birth from an age | "58-year-old woman" | No `birthDate` |
| Infer gender from a name | "Patient: John Doe" | No `gender` |
| Guess an ambiguous unit | "Weight 180" | A `valueQuantity` with no unit |
| Add a timezone the note does not state | "Seen at 09:00 on 3 November 2026" | Only the date, `"2026-11-03"`, because R4 needs a timezone with every time of day |
| Guess a field that R4 requires | No encounter class in the note | The field marks the value as unknown. See [How the response meets FHIR R4](#how-the-response-meets-fhir-r4). |

The fixed defaults are:

* A few common measurements get a default unit when the note leaves it out: a heart rate gets `beats/min`, and a temperature of 37.2 gets `°C`.
* Condition `clinicalStatus` and AllergyIntolerance `clinicalStatus` default to `active`. A Condition whose note states that it ended (an `abatement`) gets `inactive` instead of `active`, because R4 does not allow an ended condition to be active.
* Observation `status` defaults to `final`, Procedure `status` to `completed`, and FamilyMemberHistory `status` to `completed`.
* Encounter `status` defaults to `in-progress` when the note describes the encounter but not its status, and to `unknown` when the note says nothing about the encounter.

Values that your system already knows, such as the date of birth, are best sent in the request. See [Reduce extraction errors](https://docs.corti.ai/fhir/reduce-errors.md#send-what-you-already-know).

## Every value points back to the note

Every resource can carry two kinds of Corti [extensions](https://hl7.org/fhir/R4/extensibility.html): **mentions** and **alternatives**. They let a reviewer, or your code, check each value against the note. Any FHIR system that does not know these extensions ignores them, so the extensions never block an import.

### Mentions

A mention links one field of a resource to the exact words in the note that support it. For the note "58-year-old woman with type 2 diabetes, poorly controlled. BP 150/90.", the mentions on the systolic blood pressure Observation read:

| Field | Note text | Characters |
| - | - | - |
| `code` | "BP" | 59 to 61 |
| `valueQuantity` | "150" | 62 to 65 |

In JSON, each mention is one extension with the URL `https://docs.corti.ai/fhir/StructureDefinition/mention`. Inside it, `field` names the FHIR field, and each `evidence` block holds one text span. The span gives its position in the note (`start`, `end`):

```json theme={null}
{
  "url": "https://docs.corti.ai/fhir/StructureDefinition/mention",
  "extension": [
    { "url": "field", "valueString": "valueQuantity" },
    {
      "url": "evidence",
      "extension": [
        { "url": "start", "valueInteger": 62 },
        { "url": "end", "valueInteger": 65 },
        { "url": "text", "valueString": "150" }
      ]
    }
  ]
}
```

All offsets are character offsets counted from 0, with the end excluded. A field can have several `evidence` blocks when the note supports it in more than one place.

When you send the note as a string (`context_type: "other"`), `start` and `end` count inside that string. In Python, `context["data"][62:65]` returns `"150"`.

When you send sections (`context_type: "clinical_note"`), the endpoint writes each section into the note as `Title:` on its own line, then the body, then a blank line. `start` and `end` count across that whole note. Each span inside one section also gets its position in that section:

* `sectionId` is the `id` of the section you sent.
* `sectionStart` and `sectionEnd` count inside the section's title and body joined together. In Python, `(section["title"] + section["body"])[sectionStart:sectionEnd]` returns the span.

For example, with sections `{"id": "dx", "title": "Diagnoses", "body": "Type 2 diabetes."}` and `{"id": "vitals", "title": "Vitals", "body": "BP 150/90."}`, the word `BP` has `start: 37`, `end: 39`, `sectionId: "vitals"`, `sectionStart: 6` and `sectionEnd: 8`. A span that crosses from one section into the next has only `start` and `end`.

A field whose value comes from the request, such as a `patient.birthDate` you sent, or from a fixed default, such as `clinicalStatus: active`, has no mention, because the note is not its source.

### Alternatives

Sometimes more than one code could fit a mention. The endpoint picks one, and lists up to 10 runners-up in an alternative extension:

| Field | Chosen code | Alternative |
| - | - | - |
| `code` | `44054006` Type 2 diabetes mellitus | `443694000` Uncontrolled type 2 diabetes mellitus |

```json theme={null}
{
  "url": "https://docs.corti.ai/fhir/StructureDefinition/alternative",
  "extension": [
    { "url": "field", "valueString": "code" },
    {
      "url": "alternative",
      "valueCodeableConcept": {
        "coding": [
          {
            "system": "http://snomed.info/sct",
            "code": "443694000",
            "display": "Uncontrolled type 2 diabetes mellitus"
          }
        ],
        "text": "Uncontrolled type 2 diabetes mellitus"
      }
    }
  ]
}
```

Alternatives appear for `code` and `bodySite`. On FamilyMemberHistory, the field is `condition.code`. [FHIR datatypes](https://docs.corti.ai/fhir/datatypes.md#extension) lists every sub-extension.

## How the response meets FHIR R4

The response is valid FHIR R4 (4.0.1): a FHIR server that validates against R4 accepts the Bundle as it is, apart from the Corti extensions, which a server can either ignore or load from their [definition pages](https://docs.corti.ai/fhir/StructureDefinition/mention.md), and one Condition status described below. Five rules make that possible without adding anything the note does not say.

**Every resource links to the Patient.** Each entry in the Bundle has a `fullUrl` such as `urn:uuid:4f30e37b-653a-5a85-8974-753b6acbef6a`, which matches the resource's `id`. Clinical resources point to the Patient with `subject` (Condition, Procedure, Observation, and the Encounter) or `patient` (AllergyIntolerance and FamilyMemberHistory), and Condition, Procedure, and Observation point to the Encounter with `encounter`. Each `reference` names a `fullUrl` in the same Bundle.

**The Bundle is a transaction.** The Bundle has `type: transaction`, and every entry has a `request` that creates its resource. To store the resources, POST the Bundle as it is to the base URL of your FHIR server. The server creates every resource and replaces the `urn:uuid` references with the IDs it assigns, as for any FHIR [transaction](https://hl7.org/fhir/R4/http.html#transaction). You do not need to change the Bundle first. The Patient and the Encounter are conditional creates: their `request` has an `ifNoneExist` with their identifier, so when your server already holds a Patient or Encounter with that identifier, the new resources link to it instead of creating a duplicate. Send your own IDs in the request to make use of this. See [Reduce extraction errors](https://docs.corti.ai/fhir/reduce-errors.md#send-what-you-already-know).

**A required field the note does not state is marked unknown.** R4 requires some fields that a note often leaves out. Instead of guessing a value, the endpoint returns the field with only the standard [data-absent-reason](https://hl7.org/fhir/R4/extension-data-absent-reason.html) extension:

```json theme={null}
"class": {
  "extension": [
    { "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", "valueCode": "unknown" }
  ]
}
```

This applies to Encounter `class`, FamilyMemberHistory `relationship`, AllergyIntolerance `reaction.manifestation`, and Procedure `performer.actor`. Optional fields the note does not state, such as Patient `name`, are left out.

**A time of day needs a timezone.** R4 accepts a time only with seconds and a timezone offset. Notes rarely state a timezone, so a time without one is cut back to its date: "09:00 on 3 November 2026" becomes `"2026-11-03"`. The words stay in the field's [mention](#mentions). A time that comes with an offset keeps it. Fields that have no text variant, such as AllergyIntolerance `lastOccurrence` and Observation `effectiveDateTime`, leave out free text such as "last month".

**Statuses never contradict each other.** A Condition with an `abatement` gets `inactive` instead of `active`, and a Condition or AllergyIntolerance with the `verificationStatus` `entered-in-error` carries no `clinicalStatus`, as R4 requires.

**One Condition status is outside R4.** The endpoint can return Condition `clinicalStatus` `unknown`. The [R4 value set](https://hl7.org/fhir/R4/valueset-condition-clinical.html) has a required binding and does not include `unknown`, so a strict R4 validator rejects it. Map `unknown` to a status your system accepts before you import the Bundle into a strict R4 server.

## Next steps

> [card] [Reduce extraction errors](https://docs.corti.ai/fhir/reduce-errors.md)
>
> Send what you know, skip structured data, and review the results.

> [card] [Filter FHIR resources](https://docs.corti.ai/fhir/filtering.md)
>
> Produce only the resources you need and restrict the codes they carry.


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