• 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
ProductsIntegrations AI Interface Authentication Mobile Development Documentation GraphAPI
DocumentationAPI SDK Quickstart
CommunityBlog Research Community Podcast Github
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

August 14, 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. That's not a pipeline. That's a job.

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

September 2025 updates

October 1, 2025

September 2025 updates

July: Terra Research launches, Lab Reports land in the dashboard with PDF/Image → JSON, and Samsung Health moves to the new Data SDK for a tighter Android integration. 🚀

Alex VenetidisAlex Venetidis
August 2025 updates

September 1, 2025

August 2025 updates

🎉 July Highlights: InBody Goes Global, Faster APIs, and Rock-Solid Data 💪📊

Alex VenetidisAlex Venetidis
July 2025 updates

August 2, 2025

July 2025 updates

July = rock-solid Terra: WHOOP V2, Garmin & Fitbit bug fixes, faster SDKs, plus bulk blood-report uploads with smarter reference ranges. Reliability + data power-ups! 💪🩸

Alex VenetidisAlex Venetidis

More Topics

All Blogs
Team Spotlight
Startup Spotlight
How To
Blog
Podcast
Product Updates
Wearables
See All >
Blood Test PDF to Structured Biomarkers: Lab Reports API

Blood Test PDF to Structured Biomarkers: Lab Reports API

Terra now turns lab report PDFs into structured biomarker data: canonical keys, LOINC codes, UCUM units and reference ranges, delivered through your existing payloads.

Terra APITerra API
August 14, 2026
Health data API integration: 12 lessons on Terra's Unified API

Health data API integration: 12 lessons on Terra's Unified API

Health data API integration lessons from a production build on Terra's Unified API: webhook idempotency, storing health data, merging two devices.

Maximilian MatthewsMaximilian Matthews
July 30, 2026
The complete guide: How the new Google Health API works

The complete guide: How the new Google Health API works

Google Health API replaces the Fitbit Web API. This is the field guide with code, schemas, and a migration playbook to help you understand where Google Health is heading.

Vanessa NeeffVanessa Neeff
May 18, 2026
September 2025 updates

September 2025 updates

July: Terra Research launches, Lab Reports land in the dashboard with PDF/Image → JSON, and Samsung Health moves to the new Data SDK for a tighter Android integration. 🚀

Alex VenetidisAlex Venetidis
October 1, 2025
August 2025 updates

August 2025 updates

🎉 July Highlights: InBody Goes Global, Faster APIs, and Rock-Solid Data 💪📊

Alex VenetidisAlex Venetidis
September 1, 2025