> 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/webhook-payload.md).

# Webhook Payload

> ⚠️ **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

How Webhooks Work

When a lab report session reaches the `sent` status, Terra delivers an **event** to your configured webhook destination.

1. Configure your webhook URL in the [Terra Dashboard](https://dashboard.tryterra.co)
2. Upload a lab report via the API
3. Terra processes the report and delivers the event via HTTP POST to your endpoint
4. Your endpoint should return a `2xx` status code to acknowledge receipt

Every webhook is an **event envelope**: event metadata (`type`, `event_id`, `occurred_at`, `upload_id`) wraps the report itself under `data`:

```json
{
  "type": "lab_report.completed",
  "event_id": "evt_9f1c…",
  "occurred_at": "2026-03-28T14:23:45Z",
  "upload_id": "upl_4a2b…",
  "data": { "session_id": "…", "panels": [ … ], "results": [ … ] }
}
```

* `type` is the event: `lab_report.completed` (success) or `lab_report.failed` (failure). Branch on it in your handler.
* `event_id` is unique per event — use it to deduplicate redeliveries.
* `upload_id` ties every event from the same upload together (one upload can yield more than one report).
* `data` is a focused event resource — the report's identity, metadata, `panels`, and standardized `results`. It is **not** the full session. For the full session (status history, file/byte counts, presigned files) call `GET /v2/lab-reports/{session_id}` and its sub-resources.

{% hint style="info" %}
**Result shape.** Each result is grouped into four layers: `source` (what the report literally printed), `biomarker` (the normalized identity, `key` is `null` when unmatched), `measurement` (one typed value — check `measurement.type` and read the matching field), and `interpretation` (the coded abnormality signal). This is identical between the webhook payload and the `GET` response, so a result parses the same either way.
{% endhint %}

{% hint style="info" %}
The webhook does **not** embed presigned file URLs or per-destination delivery state — those expire / change after delivery. Fetch them on demand from the [sub-resources](/lab-reports/api-reference.md) (`/files`, `/deliveries`) using the `session_id` from `data`.
{% endhint %}

## Terra-Reference Header

Webhook requests include a `Terra-Reference` header containing the session ID. You can use this for logging and deduplication before parsing the body.

```
Terra-Reference: 297405620317847552
```

## Complete Payload Example

```json
{
  "type": "lab_report.completed",
  "event_id": "evt_9f1c2d7a8b3e4f56",
  "occurred_at": "2026-03-28T14:23:45Z",
  "upload_id": "upl_4a2b8c1d",
  "data": {
    "session_id": "297405620317847552",
    "reference_id": "patient_456",
    "report_date": "2026-03-15",
    "report_time": "09:30",
    "report_locale": "en-US",
    "results_count": 4,
    "report_notes": "Patient fasting for 12 hours prior to collection.",
    "panels": [
      { "id": 1, "name": "CBC", "key": "cbc" },
      { "id": 2, "name": "Thyroid", "key": "thyroid" },
      { "id": 3, "name": "Tumor Markers", "key": "tumor markers" }
    ],
    "results": [
      {
        "source": {
          "name": "Haemoglobin (Hb)",
          "panel": "CBC",
          "value": "14.2",
          "units": "g/dL",
          "flag": "H",
          "method": "Photometry",
          "reference_text": "13.0 - 17.0 (Male)",
          "collection_date": "2026-03-15",
          "collection_time": "08:15"
        },
        "biomarker": {
          "key": "hemoglobin_blood",
          "display_name": "Hemoglobin",
          "loinc_code": "718-7",
          "panel_id": 1,
          "panel_key": "cbc",
          "specimen": "blood"
        },
        "measurement": {
          "type": "numeric",
          "numeric": 14.2,
          "units": "g/dL",
          "ucum_code": "g/dL"
        },
        "interpretation": {
          "flag": "high",
          "flag_raw": "H",
          "source": "report",
          "applied_range": { "lower": 13.0, "upper": 17.0 }
        },
        "reference_ranges": [
          {
            "lower": 13.0,
            "upper": 17.0,
            "type": "normal",
            "context": { "sex": "male", "age_lower": 18, "modifiers": ["fasting"] }
          }
        ]
      },
      {
        "source": {
          "name": "Thyroid Stimulating Hormone",
          "panel": "Thyroid",
          "value": "2.45",
          "units": "mIU/L"
        },
        "biomarker": {
          "key": "tsh",
          "display_name": "TSH",
          "loinc_code": "3016-3",
          "panel_id": 2,
          "panel_key": "thyroid",
          "specimen": "blood"
        },
        "measurement": {
          "type": "numeric",
          "numeric": 2.45,
          "units": "mIU/L",
          "ucum_code": "m[IU]/L"
        },
        "interpretation": {
          "flag": null,
          "source": "none"
        },
        "reference_ranges": [
          { "lower": 0.27, "upper": 4.2, "type": "normal", "context": { "age_lower": 18 } },
          {
            "lower": 0.2,
            "upper": 3.0,
            "type": "normal",
            "context": { "sex": "female", "age_lower": 18, "pregnancy_status": "trimester_1" }
          }
        ]
      },
      {
        "source": {
          "name": "Hepatitis B Surface Antigen",
          "value": "Negative",
          "method": "ECLIA"
        },
        "biomarker": {
          "key": "hepatitis_b_surface_antigen",
          "display_name": "Hepatitis B Surface Antigen",
          "loinc_code": null,
          "specimen": "blood"
        },
        "measurement": {
          "type": "qualitative",
          "qualitative": { "text": "Negative" }
        },
        "interpretation": {
          "flag": "normal",
          "flag_raw": "Negative",
          "source": "report"
        },
        "reference_ranges": []
      },
      {
        "source": {
          "name": "Prostate Specific Antigen",
          "panel": "Tumor Markers",
          "value": "<0.04",
          "units": "ng/mL",
          "reference_text": "0.0 - 4.0 (Male)"
        },
        "biomarker": {
          "key": "prostate_specific_antigen_total",
          "display_name": "PSA",
          "loinc_code": "2857-1",
          "panel_id": 3,
          "panel_key": "tumor markers",
          "specimen": "blood"
        },
        "measurement": {
          "type": "bounded",
          "bounded": { "operator": "lt", "value": 0.04 },
          "units": "ng/mL",
          "ucum_code": "ng/mL"
        },
        "interpretation": {
          "flag": null,
          "source": "none",
          "applied_range": { "lower": 0.0, "upper": 4.0 }
        },
        "reference_ranges": [
          {
            "lower": 0.0,
            "upper": 4.0,
            "type": "normal",
            "context": { "sex": "male", "age_lower": 40, "age_upper": 49 }
          }
        ]
      }
    ]
  }
}
```

## Failure Webhook

If a report can't be processed — an unreadable file, an extraction or standardization error, or an internal error — Terra delivers a **failure** webhook to the same destination(s) instead of the success payload, so you're notified rather than left waiting.

Success and failure are distinguished by the envelope `type`: `lab_report.completed` for a completed report, `lab_report.failed` for a failure. Branch on `type` in your handler. A failure event is the same envelope; its `data` carries the `session_id`, `reference_id`, and an `error` object.

```json
{
  "type": "lab_report.failed",
  "event_id": "evt_7b3e1a9c4d2f",
  "occurred_at": "2026-03-28T14:23:45Z",
  "upload_id": "upl_4a2b8c1d",
  "data": {
    "session_id": "297405620317847552",
    "reference_id": "patient_456",
    "error": {
      "code": "extraction_failed",
      "message": "Failed to extract biomarker data from the uploaded file.",
      "retriable": true
    }
  }
}
```

### Error Object

| Field       | Type    | Description                                                             |
| ----------- | ------- | ----------------------------------------------------------------------- |
| `code`      | string  | Machine-readable failure code (see below)                               |
| `message`   | string  | Human-readable description of the failure                               |
| `retriable` | boolean | `true` if re-uploading the same report may succeed; `false` if it won't |

### Error Codes

| Code                     | Retriable | Meaning                                                            |
| ------------------------ | --------- | ------------------------------------------------------------------ |
| `file_unreadable`        | `false`   | The uploaded file couldn't be read — upload a clearer scan/export. |
| `extraction_failed`      | `true`    | Biomarker extraction failed; a retry may succeed.                  |
| `standardization_failed` | `true`    | Biomarker standardization failed; a retry may succeed.             |
| `internal`               | `true`    | An internal error occurred; a retry may succeed.                   |

## Field Specifications

### Event Envelope

| Field         | Type   | Description                                                                  |
| ------------- | ------ | ---------------------------------------------------------------------------- |
| `type`        | string | `lab_report.completed` or `lab_report.failed`                                |
| `event_id`    | string | Unique per event — deduplicate redeliveries on it                            |
| `occurred_at` | string | When the event occurred (ISO-8601, UTC)                                      |
| `upload_id`   | string | Correlates every event produced from the same upload                         |
| `data`        | object | The report resource — the Data Object below (completed) or an error (failed) |

### Data Object

The `data` of a `lab_report.completed` event. This is a focused event resource — the report's identity, metadata, and standardized `results` — **not** the full session. For the full session (status history, file/byte counts, presigned files) call `GET /v2/lab-reports/{session_id}` and its sub-resources. Input files, the thumbnail, and per-destination delivery state are **not** here — fetch them on demand from the [`/files`, `/artifacts`, and `/deliveries` sub-resources](/lab-reports/api-reference.md).

| Field           | Type      | Description                                                                                            |
| --------------- | --------- | ------------------------------------------------------------------------------------------------------ |
| `session_id`    | string    | Snowflake int64 ID (as string) — always present                                                        |
| `reference_id`  | string    | Your external identifier (from upload query param)                                                     |
| `report_date`   | string    | Date from the lab report (YYYY-MM-DD)                                                                  |
| `report_time`   | string    | Time from the lab report (HH:MM 24-hour)                                                               |
| `report_locale` | string    | Locale of the report (e.g. `"ja-JP"`, `"pt-BR"`)                                                       |
| `results_count` | integer   | Number of biomarker results — always present                                                           |
| `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 — always present                                                     |
| `report_notes`  | string    | Free-text notes extracted from the report                                                              |

{% 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 %}

### Panel Object

A panel is a report-level grouping (e.g. "CBC"). Results point at a panel by `panel_id`; join on it to group results under their panel.

| Field  | Type    | Description                                                              |
| ------ | ------- | ------------------------------------------------------------------------ |
| `id`   | integer | Report-local panel id — matches `biomarker.panel_id` on results          |
| `name` | string  | Verbatim panel header as printed on the report                           |
| `key`  | string? | Normalized dedup key for the panel (a stable text key, not a LOINC code) |

### Result Object

Each result is grouped into four layers plus its reference ranges. **`source`** is what the report literally printed, **`biomarker`** is the normalized identity, **`measurement`** is the single typed value, and **`interpretation`** is the coded abnormality signal.

**`source`** — verbatim provenance:

| Field             | Type    | Description                                                  |
| ----------------- | ------- | ------------------------------------------------------------ |
| `name`            | string  | Exact biomarker name as printed on the report                |
| `panel`           | string? | Verbatim panel header (omitted if the result isn't panelled) |
| `value`           | string? | The value exactly as printed on the report                   |
| `units`           | string? | Units as printed on the report                               |
| `flag`            | string? | Raw flag text as printed (e.g. `"H"`, `"↑"`, `"Négatif"`)    |
| `method`          | string? | Lab methodology (e.g. "ECLIA")                               |
| `notes`           | string? | Additional lab comments                                      |
| `reference_text`  | string? | Human-readable reconstruction of the printed reference range |
| `collection_date` | string? | Specimen collection date for this result (YYYY-MM-DD)        |
| `collection_time` | string? | Specimen collection time for this result (HH:MM 24-hour)     |

**`biomarker`** — normalized identity:

| Field          | Type           | Description                                                                      |
| -------------- | -------------- | -------------------------------------------------------------------------------- |
| `key`          | string \| null | Canonical biomarker slug; always present, **null** when unmatched                |
| `display_name` | string?        | Standardized English name                                                        |
| `loinc_code`   | string?        | LOINC code (omitted if not mapped)                                               |
| `panel_id`     | integer?       | References the `panels[].id` this result belongs to                              |
| `panel_key`    | string?        | Normalized panel key (see Panel Object)                                          |
| `specimen`     | string?        | `"blood"`, `"serum"`, `"plasma"`, `"urine"`, `"saliva"`, `"stool"`, or `"other"` |

**`measurement`** — one typed value. Read `type`, then the matching field:

| Field           | Type    | Description                                                                       |
| --------------- | ------- | --------------------------------------------------------------------------------- |
| `type`          | string  | `"numeric"`, `"bounded"`, `"qualitative"`, `"text"`, or `"absent"`                |
| `numeric`       | number? | Present when `type` is `"numeric"`                                                |
| `bounded`       | object? | Present when `type` is `"bounded"`: `{ "operator": "lt"\|"gt", "value": number }` |
| `qualitative`   | object? | Present when `type` is `"qualitative"`: `{ "text": string }`                      |
| `text`          | string? | Present when `type` is `"text"`                                                   |
| `absent_reason` | string? | Present when `type` is `"absent"` (why the value is missing; may be null)         |
| `units`         | string? | Units of the numeric/bounded magnitude                                            |
| `ucum_code`     | string? | UCUM-compliant unit code                                                          |

**`interpretation`** — the coded "is this abnormal" signal:

| Field           | Type           | Description                                                                                                                                         |
| --------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `flag`          | string \| null | Coded abnormality (see Interpretation Flag enum); **null** when there is no signal                                                                  |
| `flag_raw`      | string?        | The lab's verbatim flag text the code was derived from                                                                                              |
| `source`        | string         | Where the flag came from: `"report"` (normalized from the printed flag) or `"none"`                                                                 |
| `applied_range` | object?        | The reference range the flag was judged against: `{ "lower": number?, "upper": number? }` — populated when exactly one range applies to the patient |

| Field              | Type      | Description                      |
| ------------------ | --------- | -------------------------------- |
| `reference_ranges` | object\[] | Array of reference range objects |

### Reference Range Object

| Field     | Type    | Description                                             |
| --------- | ------- | ------------------------------------------------------- |
| `lower`   | number? | Lower limit (omitted for one-sided ranges like "< 4.0") |
| `upper`   | number? | Upper limit (omitted for one-sided ranges like "> 1.0") |
| `type`    | string  | Range-type label — see enum values below                |
| `context` | object? | Demographic context for when this range applies         |

### Context Object

| Field                    | Type      | Description                                                                         |
| ------------------------ | --------- | ----------------------------------------------------------------------------------- |
| `sex`                    | string    | `"male"`, `"female"`, or omitted (applies to all)                                   |
| `age_lower`              | integer?  | Minimum age in years this range applies to                                          |
| `age_upper`              | integer?  | Maximum age in years (omitted = no upper limit)                                     |
| `pregnancy_status`       | object?   | Pregnancy status: `"trimester_1"`, `"trimester_2"`, or `"trimester_3"` (or omitted) |
| `cycle_phase`            | object?   | Menstrual cycle phase (or omitted)                                                  |
| `gestational_week_lower` | integer?  | Lower gestational week bound (omitted if not applicable)                            |
| `gestational_week_upper` | integer?  | Upper gestational week bound (omitted if not applicable)                            |
| `reference_population`   | object?   | Reference population (e.g. `"nhanes_iii"`, `"young_adult"`; omitted if not set)     |
| `modifiers`              | string\[] | Additional qualifiers (e.g. `["fasting", "supine"]`)                                |

## Enum Reference

### Status

| Value             | Description                                              |
| ----------------- | -------------------------------------------------------- |
| `processing`      | Extracting biomarker data from the file                  |
| `processed`       | Extraction complete                                      |
| `standardizing`   | Matching biomarkers and normalizing units                |
| `standardized`    | Standardization complete, results queryable              |
| `sending`         | Delivering payload to webhook                            |
| `sent`            | Delivered to all configured destinations                 |
| `partially_sent`  | Delivered to at least one destination, but failed others |
| `failed`          | An error occurred                                        |
| `retry_scheduled` | Retry has been scheduled                                 |
| `retrying`        | Retry in progress                                        |
| `cancelled`       | Processing was cancelled                                 |
| `deleted`         | Session has been deleted                                 |

### Reference Range Type

Values for `reference_ranges[].type` — the label describes the range, not a verdict on the result.

| Value             | Description              |
| ----------------- | ------------------------ |
| `normal`          | Within normal range      |
| `low`             | Below normal             |
| `high`            | Above normal             |
| `borderline_low`  | Borderline low           |
| `borderline_high` | Borderline high          |
| `critical_low`    | Critically low           |
| `critical_high`   | Critically high          |
| `abnormal`        | Abnormal                 |
| `therapeutic`     | Within therapeutic range |
| `subtherapeutic`  | Below therapeutic range  |
| `toxic`           | Toxic level              |

### Interpretation Flag

Values for `interpretation.flag` — the coded per-result abnormality signal (`null` when there is no signal).

| Value             | Description                    |
| ----------------- | ------------------------------ |
| `normal`          | Not flagged                    |
| `low`             | Below range                    |
| `high`            | Above range                    |
| `borderline_low`  | Borderline low                 |
| `borderline_high` | Borderline high                |
| `critical_low`    | Critically low                 |
| `critical_high`   | Critically high                |
| `abnormal`        | Flagged, direction unspecified |

### Specimen Type

| Value    | Description |
| -------- | ----------- |
| `blood`  | Blood       |
| `serum`  | Serum       |
| `plasma` | Plasma      |
| `urine`  | Urine       |
| `saliva` | Saliva      |
| `stool`  | Stool       |
| `other`  | Other       |

### Result Type

| Value         | Description           |
| ------------- | --------------------- |
| `numeric`     | Numeric measurement   |
| `qualitative` | Categorical outcome   |
| `text`        | Free-text observation |

### Biological Sex

| Value    | Description |
| -------- | ----------- |
| `male`   | Male        |
| `female` | Female      |

### Report Type

| Value | Description |
| ----- | ----------- |
| `lab` | Lab report  |
