> For the complete documentation index, see [llms.txt](https://docs.tryterra.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tryterra.co/lab-reports/core-concepts.md).

# Core Concepts

> ⚠️ **Pre-Release**
>
> This page is currently under active development and is provided in a pre-release state.
>
> Some content may evolve as we continue to iterate on the implementation based on your feedback, but the core concepts and functionality are expected to remain consistent.
>
> The page will be marked as general release in the near future

## Session Lifecycle

Every lab report upload creates a **session**. The session tracks the report through extraction, standardization, and delivery.

```
                          ┌──────────────────────────────┐
                          │         Upload File          │
                          │   POST /v2/lab-reports       │
                          └──────────────┬───────────────┘
                                         │
                                         ▼
                          ┌──────────────────────────────┐
                          │        processing            │
                          │  OCR + AI extraction of      │
                          │  biomarker data from file    │
                          └──────────────┬───────────────┘
                                         │
                                         ▼
                          ┌──────────────────────────────┐
                          │        processed             │
                          │  Raw results extracted       │
                          └──────────────┬───────────────┘
                                         │
                                         ▼
                          ┌──────────────────────────────┐
                          │      standardizing           │
                          │  Fuzzy matching biomarkers,  │
                          │  normalizing units & ranges  │
                          └──────────────┬───────────────┘
                                         │
                                         ▼
                          ┌──────────────────────────────┐
                          │      standardized            │
                          │  Results ready (queryable)   │
                          └──────────────┬───────────────┘
                                         │
                                         ▼
                          ┌──────────────────────────────┐
                          │        sending               │
                          │  Delivering to destinations  │
                          └──────────────┬───────────────┘
                                         │
                                         ▼
                          ┌──────────────────────────────┐
                          │          sent                 │
                          │  Delivered (terminal)        │
                          └──────────────────────────────┘

    At any point, a failure moves the session to:

                          ┌──────────────────────────────┐
                          │         failed               │
                          │  Error (terminal)            │
                          └──────────────────────────────┘
```

### Status Reference

| Status            | Description                                           | Terminal | Results Available |
| ----------------- | ----------------------------------------------------- | -------- | ----------------- |
| `processing`      | Extracting biomarker data from the file               | No       | No                |
| `processed`       | Extraction complete                                   | No       | No                |
| `standardizing`   | Matching biomarkers and normalizing units             | No       | No                |
| `standardized`    | Standardization complete, results queryable           | No       | Yes               |
| `sending`         | Delivering the report to your configured destinations | No       | Yes               |
| `sent`            | Delivered to all opted-in destinations                | Yes      | Yes               |
| `partially_sent`  | Delivered to at least one destination, failed others  | Yes      | Yes               |
| `failed`          | An error occurred (check status history for info)     | Yes      | Partial/No        |
| `retry_scheduled` | Retry has been scheduled                              | No       | Partial/No        |
| `retrying`        | Retry in progress                                     | No       | Partial/No        |
| `cancelled`       | Processing was cancelled                              | Yes      | Partial/No        |
| `deleted`         | Session has been deleted                              | Yes      | No                |

{% hint style="info" %}
Status values are clean lowercase strings like `"sent"`, `"processing"`, `"failed"` — not proto-style names like `"REPORT_STATUS_SENT"`.
{% endhint %}

## Data Model

The data model is a hierarchy: **Session -> Results -> Reference Ranges**. Each result is grouped into four layers — `source` (verbatim from the report), `biomarker` (normalized identity), `measurement` (one typed value), and `interpretation` (coded abnormality signal).

```
Session
├── session_id              (snowflake string)
├── upload_id               (the upload this report came from)
├── reference_id            (your external ID, omitted if not set)
├── current_status          (e.g. "sent", "processing")
├── report_type             (e.g. "lab")
├── report_date             (YYYY-MM-DD string)
├── report_time             (HH:MM 24-hour)
├── collection_date         (YYYY-MM-DD string)
├── collection_time         (HH:MM 24-hour)
├── uploaded_at             (ISO-8601)
├── updated_at              (ISO-8601)
├── report_locale           (e.g. "ja-JP", "pt-BR")
├── lab_name                (string)
├── patient_sex             ("male", "female", or omitted)
├── patient_age_at_collection (integer, omitted if unknown)
├── report_notes            (free-text)
├── status_history[]        (status + timestamp + note entries)
├── results_count           (integer)
├── file_count              (integer)
├── input_bytes             (integer)
├── output_bytes            (integer)
│
├── panels[]                (report-level groupings, omitted if no panels)
│   ├── id                  (report-local panel id, matches biomarker.panel_id)
│   ├── name                (verbatim panel header)
│   └── key                 (normalized dedup key, may be null)
│
└── results[]
    ├── source                          (what the report literally printed)
    │   ├── name                        (exact biomarker name from the report)
    │   ├── panel                       (verbatim panel header, omitted if none)
    │   ├── value                       (value exactly as printed)
    │   ├── units                       (units as printed)
    │   ├── flag                        (raw flag text: "H", "L", "↑", etc.)
    │   ├── method                      (lab methodology, omitted if none)
    │   ├── notes                       (additional lab comments, omitted if none)
    │   ├── reference_text              (human-readable printed range)
    │   ├── collection_date             (YYYY-MM-DD, omitted if none)
    │   └── collection_time             (HH:MM 24-hour, omitted if none)
    │
    ├── biomarker                       (normalized identity)
    │   ├── key                         (canonical slug, null if no match)
    │   ├── display_name                (standardized English name)
    │   ├── loinc_code                  (LOINC code, omitted if not mapped)
    │   ├── panel_id                    (references panels[].id, omitted if none)
    │   ├── panel_key                   (normalized panel key, omitted if none)
    │   └── specimen                    ("blood", "serum", "plasma", "urine", etc.)
    │
    ├── measurement                     (one typed value — read type, then the field)
    │   ├── type                        ("numeric", "bounded", "qualitative", "text", "absent")
    │   ├── numeric                     (number, when type is "numeric")
    │   ├── bounded                     ({ operator: "lt"|"gt", value: number }, when "bounded")
    │   ├── qualitative                 ({ text: string }, when "qualitative")
    │   ├── text                        (string, when type is "text")
    │   ├── absent_reason               (string, when type is "absent")
    │   ├── units                       (units of the numeric/bounded magnitude)
    │   └── ucum_code                   (UCUM-compliant unit code)
    │
    ├── interpretation                  (coded "is this abnormal" signal)
    │   ├── flag                        ("high", "low", "normal", "critical_high", ..., null)
    │   ├── flag_raw                    (the lab's verbatim flag text)
    │   ├── source                      ("report" or "none")
    │   └── applied_range               ({ lower, upper }, the range the flag was judged against)
    │
    └── reference_ranges[]
        ├── lower               (numeric, omitted for one-sided ranges)
        ├── upper               (numeric, omitted for one-sided ranges)
        ├── type                ("normal", "low", "high", etc.)
        │
        └── context
            ├── sex                     ("male", "female", or omitted)
            ├── age_lower               (years, omitted if not specified)
            ├── age_upper               (years, omitted if no upper limit)
            ├── pregnancy_status        ("trimester_1", "trimester_2", "trimester_3")
            ├── cycle_phase             (omitted if not applicable)
            ├── gestational_week_lower  (integer, omitted if not applicable)
            ├── gestational_week_upper  (integer, omitted if not applicable)
            ├── reference_population    (e.g. "nhanes_iii", omitted if not set)
            └── modifiers[]             (string array, e.g. ["fasting", "supine"])
```

### Session Object

| Field                       | Type      | Description                                                                                            |
| --------------------------- | --------- | ------------------------------------------------------------------------------------------------------ |
| `session_id`                | string    | Snowflake int64 ID (as string)                                                                         |
| `upload_id`                 | string    | The upload this report came from                                                                       |
| `reference_id`              | object?   | Your external identifier (from upload query param)                                                     |
| `current_status`            | string    | Current session status (e.g. `"sent"`)                                                                 |
| `report_type`               | string    | Report type (e.g. `"lab"`)                                                                             |
| `report_date`               | object?   | Date from the lab report (YYYY-MM-DD)                                                                  |
| `report_time`               | object?   | Time from the lab report (HH:MM 24-hour)                                                               |
| `collection_date`           | object?   | Specimen collection date (YYYY-MM-DD)                                                                  |
| `collection_time`           | object?   | Specimen collection time (HH:MM 24-hour)                                                               |
| `uploaded_at`               | object?   | Session creation time (ISO-8601)                                                                       |
| `updated_at`                | object?   | Last update time (ISO-8601)                                                                            |
| `report_locale`             | object?   | Locale of the report (e.g. `"ja-JP"`, `"pt-BR"`)                                                       |
| `lab_name`                  | object?   | Name of the laboratory                                                                                 |
| `patient_sex`               | string    | `"male"`, `"female"`, or omitted if unspecified                                                        |
| `patient_age_at_collection` | integer?  | Patient age in years at specimen collection                                                            |
| `report_notes`              | string    | Free-text notes extracted from the report                                                              |
| `status_history`            | object\[] | Array of status change entries                                                                         |
| `results_count`             | integer   | Number of biomarker results                                                                            |
| `file_count`                | integer   | Number of uploaded files                                                                               |
| `input_bytes`               | integer   | Size of uploaded file(s) in bytes                                                                      |
| `output_bytes`              | integer   | Size of output data in bytes                                                                           |
| `panels`                    | object\[] | Report-level panels that results reference by `panel_id` (omitted if the report has no panel grouping) |
| `results`                   | object\[] | Array of biomarker result objects                                                                      |

{% hint style="info" %}
`client_id` is intentionally omitted from the API response. The API authenticates by client identity, so echoing it back is redundant.
{% endhint %}

## Biomarker Standardization

When a lab report is processed, each extracted result goes through **biomarker standardization**:

1. **Fuzzy Matching** — The `source.name` from the report is matched against Terra's reference dataset of 4,200+ known biomarkers using fuzzy string matching.
2. **Key Assignment** — If a match is found, `biomarker.key` is set to a canonical slug (e.g., `"hemoglobin"`, `"glucose_fasting"`). If no confident match exists, `biomarker.key` is `null`.
3. **Display Name** — A standardized English `biomarker.display_name` is assigned from the matched biomarker.
4. **Unit Normalization** — The `source.units` (as printed on the report) are mapped to a UCUM-compliant `measurement.ucum_code` where possible.

{% hint style="warning" %}
`biomarker.key` can be `null` for uncommon or lab-specific tests. Always handle this case in your code — use `source.name` as a fallback for display.
{% endhint %}

See [Biomarker Reference](/lab-reports/biomarker-reference.md) for details on the matching system and a list of common biomarkers.

## UCUM Unit Codes

Terra maps lab report units to [UCUM (Unified Code for Units of Measure)](https://ucum.org/) codes wherever possible. This gives you a machine-readable, internationally standardized representation of the unit.

| Report Prints | `ucum_code` |
| ------------- | ----------- |
| g/dL          | `g/dL`      |
| mg/dL         | `mg/dL`     |
| mmol/L        | `mmol/L`    |
| 10^9/L        | `10*9/L`    |
| mIU/L         | `m[IU]/L`   |
| pg/mL         | `pg/mL`     |

When UCUM mapping is not possible (e.g., proprietary lab units), `measurement.ucum_code` is omitted and `source.units` still reflects what the report printed.

## Measurement Types

Every result carries a single `measurement` object. Read `measurement.type`, then read the matching field:

| `measurement.type` | Read this field             | Description                                                  | Example                       |
| ------------------ | --------------------------- | ------------------------------------------------------------ | ----------------------------- |
| `numeric`          | `measurement.numeric`       | A numeric measurement with optional units                    | Hemoglobin: 14.2 g/dL         |
| `bounded`          | `measurement.bounded`       | A one-sided bound: `{ operator: "lt"\|"gt", value: number }` | PSA: `<0.04`                  |
| `qualitative`      | `measurement.qualitative`   | A categorical outcome: `{ text: string }`                    | HIV screen: Non-Reactive      |
| `text`             | `measurement.text`          | Free-text observation                                        | Morphology: Normal appearance |
| `absent`           | `measurement.absent_reason` | No value could be extracted                                  | Value not reported            |

`measurement.units` and `measurement.ucum_code` describe the magnitude for `numeric`/`bounded` results. For a report showing `<0.01`, `measurement.type` is `"bounded"` and `measurement.bounded` is `{ "operator": "lt", "value": 0.01 }`; for `>5.0` it is `{ "operator": "gt", "value": 5.0 }`.

## Interpretation Flag

Each result's `interpretation` object carries the coded per-result abnormality signal. `interpretation.flag` is a **normalized** code (or `null` when there is no signal), while `interpretation.flag_raw` preserves the lab's verbatim flag text and `source.flag` carries that raw text on the source layer. `interpretation.source` is `"report"` when the flag was normalized from the printed flag, or `"none"` when there is no signal.

| `interpretation.flag` | Meaning                        | Common raw flag(s) |
| --------------------- | ------------------------------ | ------------------ |
| `normal`              | Not flagged                    | `"N"`              |
| `high`                | Above range                    | `"H"`, `"↑"`       |
| `low`                 | Below range                    | `"L"`, `"↓"`       |
| `critical_high`       | Critically high                | `"HH"`             |
| `critical_low`        | Critically low                 | `"LL"`             |
| `borderline_high`     | Borderline high                |                    |
| `borderline_low`      | Borderline low                 |                    |
| `abnormal`            | Flagged, direction unspecified | `"A"`, `"C"`       |

`interpretation.flag` is `null` when the lab did not flag that result. The raw lab string it was derived from is preserved in `interpretation.flag_raw` and `source.flag` — those are the lab's own text (e.g. `"H"`, `"↑"`, `"Négatif"`), **not** an enum.

## Async Processing Model

Lab report processing is **fully asynchronous**:

1. **Upload** — `POST /v2/lab-reports` returns `202 Accepted` immediately with a `session_id`.
2. **Processing** — Extraction and standardization happen in the background (typically 30 seconds to 2 minutes).
3. **Delivery** — Once complete, the full session payload is delivered to your webhook. You can also poll `GET /v2/lab-reports/{session_id}` at any time.

{% hint style="info" %}
Results become available on the GET endpoint as soon as the session reaches `standardized`, even before webhook delivery.
{% endhint %}

## Delivery Destinations

Lab reports are not webhook-only. A completed report can be delivered to **any** destination type you have configured — webhooks, cloud storage (S3, GCS, Azure Blob), message queues (SQS, Kafka), and databases (MongoDB, Firestore, SQL, Supabase).

### Opting a Destination In

Delivery is **opt-in per destination**. A destination receives lab reports only if its event-type allow-list explicitly includes lab reports — destinations that don't opt in are simply skipped (they aren't marked failed, and skipping one never affects the others).

Set the allow-list with the `destination_event_types` field when you create or update a destination, and include `"lab_report"` in it:

```json
{
  "destination_event_types": ["lab_report"]
}
```

A destination's `destination_event_types` may list more than one event type. Add `"lab_report"` to an existing destination's array to start sending lab reports there alongside whatever it already receives.

### Per-Destination Outcomes

Each destination's delivery is tracked **independently**. A report fanned out to several destinations records a separate outcome (`pending` → `delivered`/`failed`) for each one, so a failure to one destination never hides that the report reached the others — and never fails the report as a whole.

When some destinations succeed and others fail, the session ends in the `partially_sent` status (terminal). Retrieve the per-destination state — including the `destination_type`, `status`, `attempt_count`, and any `last_error` — from the [`/deliveries` sub-resource](/lab-reports/api-reference.md).

### How Each Type Receives the Report

* **Storage destinations** (S3, GCS, Azure Blob, Supabase) and **document databases** (MongoDB, Firestore) receive the **full report body**.
* **Row and message destinations** (SQL, SQS, Kafka) receive a **link to the report** (a time-limited URL) rather than the full body — fetch the report from that link, or from `GET /v2/lab-reports/{session_id}`.

## Timestamps and Serialization

All timestamps in responses are **ISO-8601 format** (e.g., `"2026-03-28T14:22:10Z"`).

All enum values are serialized as **clean lowercase strings**, not proto-style names or integer codes:

* Status: `"sent"` (not `"REPORT_STATUS_SENT"` or `6`)
* Measurement type: `"numeric"` (not `"LAB_REPORT_RESULT_TYPE_NUMERIC"`)
* Specimen: `"blood"` (not `"LAB_REPORT_SPECIMEN_TYPE_BLOOD"`)
* Reference range type: `"normal"` (not `"LAB_REPORT_RANGE_CLASSIFICATION_NORMAL"`)
* Interpretation flag: `"high"` (not `"LAB_REPORT_FLAG_HIGH"`)
* Biological sex: `"male"` (not `"BIOLOGICAL_SEX_MALE"`)

The raw lab flag on `source.flag` (and mirrored in `interpretation.flag_raw`) is the lab's own string (e.g., `"H"`, `"L"`), not an enum. The normalized `interpretation.flag` **is** an enum.

{% hint style="warning" %}
**Open enums:** All enum fields (`current_status`, `report_type`, `measurement.type`, `biomarker.specimen`, `reference_ranges[].type`, `interpretation.flag`, `patient_sex`) may receive **new values** in future updates without a major version change. Your code should handle unknown enum values gracefully — for example, by treating them as a default/unknown category rather than throwing an error.
{% endhint %}

Optional fields that are unset are **omitted** from the JSON response rather than appearing as `null`.

## Session IDs

Session IDs are **snowflake int64** values. In JSON they are serialized as **strings** to prevent precision loss in languages like JavaScript where `Number` cannot represent the full int64 range.

```json
{
  "session_id": "297405620317847552"
}
```

Always store and compare session IDs as strings.
