Best Practices
Recommendations for robust integration with the Lab Reports API.
Idempotency via Event ID
Every webhook event carries a unique event_id. Use it to deduplicate webhook deliveries and prevent processing the same event twice.
def handle_webhook(event):
event_id = event["event_id"]
if already_processed(event_id):
return 200 # Acknowledge but skip
process_results(event["data"]["results"])
mark_processed(event_id)
return 200Handling Unmatched Biomarkers
When biomarker.key is null, the system could not confidently match the extracted name to a known biomarker. This is normal for uncommon or lab-specific tests.
for result in session["results"]:
biomarker = result["biomarker"]
biomarker_key = biomarker["key"]
source_name = result["source"]["name"]
if biomarker_key is not None:
# Matched — the canonical key, scoped by site and by the date the
# measurement was taken. On a lab report both extras are absent and this
# collapses to the key alone; on a scan they are what keeps the same
# measure at fifteen different sites fifteen distinct rows.
store_by_biomarker(
(biomarker_key,
biomarker.get("region"),
result["source"].get("collection_date")),
result,
)
else:
# Unmatched — fall back to the name printed on the report
store_by_name(source_name, result)
log_unmatched(source_name)Key on (key, region, collection_date), not on key alone. On a lab report a biomarker appears once, so biomarker.key is unique and the extra fields are absent. On a DXA scan the same measure is reported once per anatomical site — a single scan can carry fat_mass sixty-plus times — and serial scans repeat the whole set per scan date. Keying on key alone silently keeps one row and discards the rest. Nothing errors; the data is just quietly gone. See Scans (DEXA).
Do not discard results where biomarker.key is null. They may contain clinically relevant data. Display them using source.name and flag them for manual review if needed.
Reference Range Interpretation
First, check how many ranges there are. One range is a normal interval. Several ranges are bands — Quest's HbA1c comes as <5.7, 5.7–6.4, ≥6.5 — and the one in the middle is not "normal"; read each band's label ("Consistent with increased risk for diabetes (prediabetes)") and its coded type where present. Find the band the value falls in yourself; interpretation.applied_range is only set when exactly one range applies to the patient's demographics.
Reference ranges also include demographic context. Match the range to your patient's demographics before interpreting:
Webhook Error Handling
Your webhook endpoint should:
Return 2xx quickly — Do heavy processing asynchronously after acknowledging
Be idempotent — The same payload may be delivered more than once
Handle retries — If your endpoint returns a non-2xx status, Terra will retry delivery
If your webhook is consistently failing, use the REST API (GET /v2/reports) to retrieve sessions you may have missed. Sessions remain available regardless of webhook delivery status.
Choosing Delivery Destinations
Webhooks aren't the only delivery option — lab reports can also be delivered to cloud storage, message queues, and databases. Delivery is opt-in per destination: a destination receives reports only if its destination_event_types allow-list includes "lab_report". Each destination's outcome is tracked independently, so one failing never fails the others (the session ends partially_sent in that case). Inspect per-destination state via the /deliveries sub-resource. See Delivery Destinations for the full mechanism.
UTF-8 Encoding
Lab report data may contain:
Scientific symbols:
µ,×,±Superscript notation:
10^9,10^12International characters in biomarker names and notes
Special units:
µmol/L,µg/dL
Always parse response bodies as UTF-8. Most HTTP clients do this by default, but verify your JSON parser handles multibyte characters correctly.
Rate Limits
The Lab Reports API enforces rate limits to ensure fair usage:
File size
20 MB per file
Files per request
1 (single file)
Processing time
Typically 30s–3min
Storing Session IDs
Session IDs are snowflake int64 values serialized as JSON strings (e.g., "297405620317847552"). Store them as strings, not integers:
Polling vs. Webhooks
Webhook
Real-time, no wasted requests
Requires public endpoint, retry logic
Polling
Simple, no infrastructure needed
Wastes requests, adds latency
Recommendation: Use webhooks for production. Use polling only during development or as a fallback when your webhook endpoint is unavailable.
If you must poll, check no more frequently than every 5 seconds:
Last updated
Was this helpful?