> 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/api-reference.md).

# API Reference

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

**Base URL:** `https://access.tryterra.co/api`

All endpoints require the following headers:

| Header      | Type   | Required | Description             |
| ----------- | ------ | -------- | ----------------------- |
| `dev-id`    | string | Yes      | Your Terra developer ID |
| `x-api-key` | string | Yes      | Your Terra API key      |

***

## Upload a Lab Report

<mark style="color:green;">`POST`</mark> `/v2/lab-reports`

Upload a single lab report file for processing. The file is sent as multipart form data. Accepted formats: PDF, PNG, JPEG, GIF, WebP.

### Headers

| Header         | Value                 |
| -------------- | --------------------- |
| `dev-id`       | Your developer ID     |
| `x-api-key`    | Your API key          |
| `Content-Type` | `multipart/form-data` |

### Query Parameters

| Name           | Type   | Required | Description                                      |
| -------------- | ------ | -------- | ------------------------------------------------ |
| `reference_id` | string | No       | Your external identifier for this report/patient |

### Request Body (multipart)

| Field  | Type | Required | Description                                           |
| ------ | ---- | -------- | ----------------------------------------------------- |
| `file` | file | Yes      | File to upload (PDF, PNG, JPEG, GIF, WebP; max 20 MB) |

{% hint style="warning" %}
The form field name is `file` (singular). Currently only a single file per request is supported. Multi-file support is planned.
{% endhint %}

### Example

```bash
curl -X POST "https://access.tryterra.co/api/v2/lab-reports?reference_id=patient_456" \
  -H "dev-id: YOUR_DEV_ID" \
  -H "x-api-key: YOUR_API_KEY" \
  -F "file=@/path/to/report.pdf"
```

### Response — 202 Accepted

```json
{
  "upload_id": "upl_4a2b8c1d",
  "current_status": "processing"
}
```

{% hint style="info" %}
The upload returns an `upload_id`, not a `session_id`. A single upload can produce more than one report (e.g. a multi-report PDF), so the session id(s) aren't known at upload time. Learn the `session_id`(s) from the webhook events, or list them with `GET /v2/lab-reports?upload_id=upl_4a2b8c1d`. Every resulting session and webhook event carries the same `upload_id`. `session_id` still exists — it's the per-report identifier you use for `GET /v2/lab-reports/{session_id}` — it's just no longer the handle returned at upload.

The list is eventually consistent: immediately after upload the sessions may not all exist yet, so a `GET ?upload_id=…` right after the `202` can return an empty or partial list. Webhook events are the reliable signal that a report finished.
{% endhint %}

### Error Responses

| Code | Description                               |
| ---- | ----------------------------------------- |
| 400  | Missing file or unsupported format        |
| 401  | Invalid or missing `dev-id` / `x-api-key` |
| 413  | File exceeds 20 MB                        |
| 429  | Rate limit exceeded                       |
| 500  | Internal server error                     |

Error responses use [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) Problem JSON format:

```json
{
  "type": "about:blank",
  "title": "bad request",
  "detail": "missing file field",
  "instance": "/v2/lab-reports"
}
```

***

## List Sessions

<mark style="color:blue;">`GET`</mark> `/v2/lab-reports`

List lab report sessions for your account, with optional filters.

### Query Parameters

| Name               | Type   | Required | Description                                                         |
| ------------------ | ------ | -------- | ------------------------------------------------------------------- |
| `reference_id`     | string | No       | Filter by your external reference ID                                |
| `upload_id`        | string | No       | Filter by the upload handle — returns every report from that upload |
| `report_date_from` | string | No       | Lab report date lower bound (ISO-8601 `YYYY-MM-DD`)                 |
| `report_date_to`   | string | No       | Lab report date upper bound (ISO-8601 `YYYY-MM-DD`)                 |
| `uploaded_at_from` | string | No       | Upload date lower bound (ISO-8601 `YYYY-MM-DD`)                     |
| `uploaded_at_to`   | string | No       | Upload date upper bound (ISO-8601 `YYYY-MM-DD`)                     |

All date filters are inclusive and can be combined. For example, you can query "reports from January uploaded this week" by setting both `report_date_from`/`report_date_to` and `uploaded_at_from`/`uploaded_at_to`.

{% hint style="info" %}
Results are capped at 500 per request. Use date range filters to narrow results for accounts with many sessions.
{% endhint %}

### Example

```bash
curl "https://access.tryterra.co/api/v2/lab-reports?reference_id=patient_456&uploaded_at_from=2026-03-01" \
  -H "dev-id: YOUR_DEV_ID" \
  -H "x-api-key: YOUR_API_KEY"
```

### Response — 200 OK

```json
{
  "sessions": [
    {
      "session_id": "297405620317847552",
      "upload_id": "upl_4a2b8c1d",
      "reference_id": "patient_456",
      "report_type": "lab",
      "current_status": "sent",
      "report_date": "2026-03-15",
      "uploaded_at": "2026-03-28T14:22:10Z",
      "updated_at": "2026-03-28T14:23:45Z"
    },
    {
      "session_id": "297405620317847553",
      "reference_id": "patient_456",
      "report_type": "lab",
      "current_status": "processing",
      "uploaded_at": "2026-03-30T09:11:02Z",
      "updated_at": "2026-03-30T09:11:02Z"
    }
  ]
}
```

***

## Retrieve a Session

<mark style="color:blue;">`GET`</mark> `/v2/lab-reports/{session_id}`

Retrieve the report: metadata, results, reference ranges, and status history. This response is **immutable and cacheable** — it does not include presigned file URLs (which expire) or per-destination delivery state (which changes). Fetch those from the sub-resources below.

### Path Parameters

| Name         | Type   | Required | Description                |
| ------------ | ------ | -------- | -------------------------- |
| `session_id` | string | Yes      | The session's snowflake ID |

### Example

```bash
curl "https://access.tryterra.co/api/v2/lab-reports/297405620317847552" \
  -H "dev-id: YOUR_DEV_ID" \
  -H "x-api-key: YOUR_API_KEY"
```

### Response — 200 OK

See the [Quick Start](/lab-reports/quickstart.md) page for a complete example response. The session object is returned directly at the top level. Key fields include:

* `session_id` — Snowflake ID (string)
* `upload_id` — The upload this report came from (string)
* `reference_id` — Your external reference (omitted if not set)
* `current_status` — Current status as a clean lowercase string (e.g. `"sent"`, `"processing"`)
* `report_type` — Report type (e.g. `"lab"`)
* `report_date` — Date from the lab report (YYYY-MM-DD string, omitted if not extracted)
* `report_time` — Time from the lab report (HH:MM 24-hour, omitted if not extracted)
* `collection_date` — Specimen collection date (YYYY-MM-DD string, omitted if not extracted)
* `collection_time` — Specimen collection time (HH:MM 24-hour, omitted if not extracted)
* `uploaded_at` — Session creation timestamp (ISO-8601)
* `updated_at` — Last update timestamp (ISO-8601)
* `report_locale` — Locale of the report (e.g. `"ja-JP"`, `"pt-BR"`)
* `lab_name` — Name of the laboratory
* `patient_sex` — Clean lowercase string (e.g. `"male"`, `"female"`, omitted if unspecified)
* `patient_age_at_collection` — Patient age in years (integer, omitted if unknown)
* `report_notes` — Free-text notes extracted from the report
* `status_history[]` — Array of `{ status, timestamp, note }` entries
* `results_count` — Number of biomarker results
* `file_count` — Number of uploaded files
* `input_bytes` — Size of uploaded file(s) in bytes
* `output_bytes` — Size of output data in bytes
* `panels[]` — Report-level panels that results reference by `panel_id` (omitted if the report has no panel grouping)
* `results[]` — Array of biomarker results, each grouped into `source` / `biomarker` / `measurement` / `interpretation` layers (see [Core Concepts](/lab-reports/core-concepts.md) for field specs)

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

### Error Responses

| Code | Description                       |
| ---- | --------------------------------- |
| 401  | Invalid or missing authentication |
| 404  | Session not found                 |
| 500  | Internal server error             |

***

## List Deliveries

<mark style="color:blue;">`GET`</mark> `/v2/lab-reports/{session_id}/deliveries`

The per-destination delivery state for a report. A report is fanned out to every destination that has opted in to lab reports (see [Delivery Destinations](/lab-reports/core-concepts.md#delivery-destinations)), and each is tracked independently — so a failure to one never hides that the report reached the others. This is also why a session can be `partially_sent`. Delivery state is mutable (pending → delivered/failed/retrying), so it lives here rather than on the cacheable session.

### Response — 200 OK

```json
{
  "deliveries": [
    { "destination_id": "12", "destination_type": "webhook", "status": "delivered", "attempt_count": 0 },
    { "destination_id": "15", "destination_type": "s3", "status": "failed", "attempt_count": 3, "last_error": "access denied" }
  ]
}
```

| Field              | Type    | Description                                                   |
| ------------------ | ------- | ------------------------------------------------------------- |
| `destination_id`   | string  | The destination's ID                                          |
| `destination_type` | string  | The destination's type (e.g. `webhook`, `s3`)                 |
| `status`           | string  | `pending`, `delivered`, or `failed`                           |
| `attempt_count`    | integer | Retry count — `0` on the first attempt, incremented per retry |
| `last_error`       | string? | Most recent delivery error (omitted when delivered)           |

***

## List Files

<mark style="color:blue;">`GET`</mark> `/v2/lab-reports/{session_id}/files`

The uploaded input files and the report thumbnail, with **freshly minted** presigned download URLs. URLs are issued on demand because they expire — they are not embedded in the cacheable session or the webhook.

### Response — 200 OK

```json
{
  "files": [
    { "filename": "report.pdf", "presigned_url": "https://storage.googleapis.com/...signed-url..." }
  ],
  "thumbnail": { "filename": "thumbnail.png", "presigned_url": "https://storage.googleapis.com/...signed-thumbnail-url..." },
  "expires_at": "2026-03-28T15:22:10Z"
}
```

`expires_at` applies to every URL in the response. Fetch the endpoint again to mint fresh URLs.

***

## List Artifacts

<mark style="color:blue;">`GET`</mark> `/v2/lab-reports/{session_id}/artifacts`

Intermediate processing artifacts, with freshly minted presigned URLs. **Privileged keys only** — standard keys receive a 404. Same response shape as `/files` (an `artifacts[]` array plus `expires_at`).

***

## Reprocess a Session

<mark style="color:orange;">`POST`</mark> `/v2/lab-reports/{session_id}/reprocess`

Re-run extraction and standardization on an existing session. Useful if a session failed or if you want to pick up improvements in the processing pipeline.

### Path Parameters

| Name         | Type   | Required | Description                |
| ------------ | ------ | -------- | -------------------------- |
| `session_id` | string | Yes      | The session's snowflake ID |

### Example

```bash
curl -X POST "https://access.tryterra.co/api/v2/lab-reports/297405620317847552/reprocess" \
  -H "dev-id: YOUR_DEV_ID" \
  -H "x-api-key: YOUR_API_KEY"
```

### Response — 202 Accepted

```json
{
  "session_id": "297405620317847552",
  "current_status": "processing"
}
```

Includes a `Location` header pointing to the session. The session re-enters the `processing -> ... -> sent` flow. A new webhook delivery will occur on completion.

### Error Responses

| Code | Description                       |
| ---- | --------------------------------- |
| 401  | Invalid or missing authentication |
| 404  | Session not found                 |
| 500  | Internal server error             |

***

## Delete a Session

<mark style="color:red;">`DELETE`</mark> `/v2/lab-reports/{session_id}`

Soft-delete a session. The session is marked as deleted and a background process cleans up associated storage later.

### Path Parameters

| Name         | Type   | Required | Description                |
| ------------ | ------ | -------- | -------------------------- |
| `session_id` | string | Yes      | The session's snowflake ID |

### Example

```bash
curl -X DELETE "https://access.tryterra.co/api/v2/lab-reports/297405620317847552" \
  -H "dev-id: YOUR_DEV_ID" \
  -H "x-api-key: YOUR_API_KEY"
```

### Response — 204 No Content

No response body.

### Error Responses

| Code | Description                       |
| ---- | --------------------------------- |
| 401  | Invalid or missing authentication |
| 404  | Session not found                 |
| 500  | Internal server error             |
