• Unified API
  • Mobile SDK
  • Connection Widget
  • Streaming
  • Lab Reports API
  • Graph API
  • Health Scores
  • Health Rewards
  • Planned Workouts
  • Lab Testing
  • AI Interface
  • Enterprise
  • Insurance
  • Integrations
  • Research
  • Podcast
  • Blog
  • Reports
  • Events
  • Documentation
  • Community
  • Example apps
  • Wearable Data
  • About
  • Customers
  • Partners
  • Careers
  • Support
  • Pricing
Terra
Pricing
Become an integrationGet started
next ventures
pioneer fund
samsung next
y combinator
general catalyst

The world's best health apps run on Terra data

Get started
ProductsIntegrationsAI InterfaceAuthenticationMobile DevelopmentDocumentationGraphAPI
DocumentationAPISDKQuickstart
CommunityBlogResearchCommunityPodcastAuthorsGithub
CompanyAboutCareersCustomersBecome an IntegrationCookies PolicyGDPRPrivacy PolicyTerms of Purchase
© Terra API. 2026 — All rights reserved.

Cookie Preferences

Essential CookiesAlways On
Advertisement Cookies
Analytics Cookies

Crunch Time: Embrace the Cookie Monster Within!

We use cookies to enhance your browsing experience and analyse our traffic. By clicking “Accept All”, you consent to our use of cookies according to our Cookie Policy. You can change your mind any time by visiting out cookie policy.

Cookies Policy
< Blogs
Terra API
Terra API

14 August 2026

Blood Test PDF to Structured Biomarkers: Lab Reports API

TL;DR: Blood test results reach most health apps as PDFs. Which is to say, they don't reach them at all. A lab prints a report, the patient downloads it, and every value inside is invisible to your product unless a human types it in. Our Lab Reports API takes that file and returns structured biomarker data: canonical biomarker keys, LOINC codes, typed values with UCUM units, normalized flags, and demographic reference ranges. It's essentially one call in and one webhook out.

curl -X POST "https://access.tryterra.co/api/v2/lab-reports" \
  -H "dev-id: YOUR_DEV_ID" \
  -H "x-api-key: YOUR_API_KEY" \
  -F "file=@blood-test.pdf"
{ "upload_id": "upl_4a2b8c1d", "current_status": "processing" }

That's the whole integration. If you'd rather try it than read about it, the API reference has every endpoint and the quickstart gets you a parsed report in three curl commands.

This is part one of three. This post is about why lab reports were the problem in the first place. Part two covers the data model. Part three covers the pipeline.

Blood work is the missing half of health data

The health data that flows into apps automatically is the data devices produce: steps, sleep, heart rate. Continuous, abundant, born digital. Blood work is the opposite on every axis. It's sparse. A user might test twice a year, maybe less. It's dense, too. One blood draw can tell you more about a person than a month of wrist data. And it's born on paper, or on a PDF that is functionally paper.

So you get a strange inversion. The lowest stakes data in a user's health picture arrives automatically, and the highest stakes data sits in an attachment folder somewhere. Teams building preventive health, longevity, and nutrition products end up with the same lopsided product: automated end to end on the wearable side, and on the blood side, a person reading a PDF and filling in a form. 

Why OCR and string matching fail on blood test PDFs

Every team hits this problem and tries the same first move. Run the report through OCR, then match lines against a list of biomarker names. It fails three ways.

First, there is no standard lab report. None. Every lab lays results out differently. Different columns, different headers, results split across pages, footnotes that quietly change what a value means. A parser tuned on one lab's layout misreads the next one.

The second part is the naming. The same analyte shows up as any of these depending on the lab and the country:

Haemoglobin (Hb)
Hemoglobin
HGB
Hemoglobina

Match them with strings and whether two results merge depends on which spellings someone happened to add to a list, not on whether they're the same measurement. Merge wrongly and you corrupt a trend line. Fail to match and the value just vanishes. This is the same failure mode we wrote about with exercise names in strength training, and it's worse here, because here the values are clinical.

Third, units and reference ranges. Glucose can be reported in mg/dL by one lab and mmol/L by another. Ranges shift with sex, age, pregnancy, fasting status. A parser that keeps the number but drops the range has kept the useless half of the result. A haemoglobin of 14.2 means nothing on its own. You need to know what the lab considered normal for that particular person.

How the Terra Lab Reports API extracts biomarkers from a PDF

You send the file. PDF, PNG, JPEG, GIF, or WebP, up to 20 MB, in one HTTP call. Each result comes back layered, with the lab's exact printed words preserved right next to the standardized version:

{
  "source":      { "name": "Haemoglobin (Hb)", "value": "14.2",
                   "flag": "H", "reference_text": "13.0 - 17.0 (Male)" },
  "biomarker":   { "key": "hemoglobin_blood", "loinc_code": "718-7" },
  "measurement": { "type": "numeric", "numeric": 14.2, "ucum_code": "g/dL" },
  "interpretation": { "flag": "high",
                      "applied_range": { "lower": 13.0, "upper": 17.0 } }
}

The source layer is an audit trail: a clinician can always check the structured value against what the lab actually printed. The biomarker layer is a stable identity, with a LOINC code (the international standard identifier for lab observations) where one exists, so the same analyte lines up across labs. The measurement is typed and carries a UCUM unit code, so values are comparable programmatically. The interpretation is a normalized high, low, or normal flag plus the exact range that produced it.

It doesn't matter which lab produced the report or what language it's printed in. Your backend sees the same shape either way.

When a result matches nothing in our catalog, we keep it anyway:

{
  "source":    { "name": "Some Obscure Assay", "value": "3.1" },
  "biomarker": { "key": null }
}

Nothing a lab printed is ever discarded. Dropping unmatched results would just recreate the string matching bug one layer up.

One schema across blood, wearables, and sensors

Why do this inside Terra rather than as a standalone parsing tool? Because of what happens after the parse. Lab results go to the same destinations as your wearable data, joined on your own reference_id:

{ "reference_id": "patient_456", "provider": "GARMIN",
  "data": [{ "scores": { "sleep": 78, "readiness": 64 } }] }
{ "reference_id": "patient_456", "type": "lab_report.completed",
  "data": { "panels": ["cbc", "thyroid"], "results": [ ... ] } }

If you're already using Terra for wearables, there's nothing new to build here. Lab results just start showing up where your other data does. The point of that is what it lets you show. A cholesterol number on its own doesn't mean much to a user. The same number next to their own sleep and training data from the weeks around the test starts to. Getting that combination used to mean two vendors and some glue code to join them, and the glue code was always yours to maintain.

Where the line is

One thing we're explicit about, because it shaped the entire data model: Terra supplies the signal, not the clinical call. We normalize the flag the lab printed and we attach the range that was applied. Which markers matter, what counts as concerning, and what a user should do next are your calls, made by your clinicians, in your product. Terra acts as a data processor. We don't need to retain the data to deliver it.

The rest of the series

Part two goes inside a single result: the four layers of the payload, and why unmatched results ship with a null key instead of a guessed one. Part three is the pipeline: the session lifecycle, webhooks versus polling, delivery destinations, and the sharp edges worth knowing before production.

If you'd rather not wait, the quickstart gets you a parsed report in three curl commands. Contact us to enable it on your account.

Related Articles

Terra now delivers per-set strength training data

8 September 2026

Terra now delivers per-set strength training data

Terra now delivers strength workouts set by set: reps, weight, effort and rest, normalized across nine integrations including watch-sensed Garmin sets.

Vanessa Neeff
Garmin's Connect Developer Program is paused: what it means if you're building on wearable data

7 September 2026

Garmin's Connect Developer Program is paused: what it means if you're building on wearable data

Garmin paused new Connect Developer Program applications while it updates the program. What the pause covers, why Garmin data matters, how to keep shipping.

Vanessa Neeff
Terra's Routes API: Get training routes on your user's watch

6 September 2026

Terra's Routes API: Get training routes on your user's watch

Design an activity route once, and it lands on your users' watch ready to follow, with none of the usual file wrangling!

Vanessa Neeff

More Topics

All Blogs
Team Spotlight
Startup Spotlight
How To
Blog
Podcast
Product Updates
Wearables
See All >
What I Think of Apple's Health Announcement

What I Think of Apple's Health Announcement

Apple just rebuilt the Health app around Health Age, a readiness score, and $119 lab panels. Here's what changed and what it means for companies building with health data.

Alistair Brownlee
11 September 2026
Terra now delivers per-set strength training data

Terra now delivers per-set strength training data

Terra now delivers strength workouts set by set: reps, weight, effort and rest, normalized across nine integrations including watch-sensed Garmin sets.

Vanessa Neeff
8 September 2026
Garmin's Connect Developer Program is paused: what it means if you're building on wearable data

Garmin's Connect Developer Program is paused: what it means if you're building on wearable data

Garmin paused new Connect Developer Program applications while it updates the program. What the pause covers, why Garmin data matters, how to keep shipping.

Vanessa Neeff
7 September 2026
Terra's Routes API: Get training routes on your user's watch

Terra's Routes API: Get training routes on your user's watch

Design an activity route once, and it lands on your users' watch ready to follow, with none of the usual file wrangling!

Vanessa Neeff
6 September 2026
Strava API Changes in 2026: Can Developers Still Get Access?

Strava API Changes in 2026: Can Developers Still Get Access?

Strava API access in 2026: developer tiers, subscription requirements, September endpoint removals, AI restrictions and the 2027 migration deadline.

Terra API
6 September 2026