How to Pull Lab Results from the Metrc API
Lab results drive labels, certificates of analysis, product listings, and quality checks, which makes them some of the most requested data in Metrc. They are also some of the hardest to work with, because every lab names its tests a little differently. This guide covers how to pull lab results through the T3 API and how normalized keys make the values usable.
Updated
How lab results are structured
Testing laboratories record results in Metrc, and those results are attached to the packages that were tested. A single package’s results usually span many rows, one per analyte per test, and often 50 to 100 of them. In the T3 API each row includes fields such as testTypeName, testPassed, testResultLevel, overallPassed, labFacilityName, and testPerformedDate.
The hard part is naming. One lab reports “Total THC” while another reports “Total Delta-9 THC”, and THCa can appear as THCA, THC-A, or Delta-9 THCA. Code that matches raw test names ends up with an ever-growing list of special cases.
Option 1: Results for a single package
When you already know the package, request its results directly. Each of these endpoints takes a licenseNumber plus the package or harvest ID:
GET /v2/packages/labresultsreturns the individual lab result rows for apackageId.GET /v2/packages/labresult-batchesreturns the same package’s results grouped into lab result batches.GET /v2/packages/labresults/documentreturns the COA PDF for alabTestResultDocumentFileId. A package may have hundreds of results, but most share just one or two document IDs.GET /v2/harvests/labresultsreturns results for aharvestId.
These IDs are numeric Metrc IDs, not tag labels. If you only have a tag, look up the package first by filtering the collection, for example filter=label__eq:1A4000000000000000000001.
Option 2: Results on every package with include=labResults
For inventory-wide work, call a packages supercollection such as /v2/packages/active/super (on hold, inactive, and in transit packages work the same way) with include=labResults, or include=labResultBatches:
GET https://api.trackandtrace.tools/v2/packages/active/super
?licenseNumber=LIC-000123
&include=labResults
X-T3-API-Key: <your secret key>Each package comes back with its raw labResults array and a metadata object populated with derived fields:
extractedLabResults: every result with its value, unit, pass status, and normalized tags such as["total", "thc"].testSamplePackageLabels: the test sample packages associated with this package.labResultPdfs: T3 API URLs to the package’s COA PDFs, readable with an authenticated request.indexedLabResultsandhasTerpeneResults, covered below.
Normalized keys with metadata.indexedLabResults
indexedLabResults is a simplified dictionary with one entry per test, keyed by a normalized name that is the same in every state no matter how the lab spelled it:
"metadata": {
"indexedLabResults": {
"totalTHC": {
"full": "Total THC: 22.4 %",
"name": "Total THC",
"value": 22.4,
"unit": "%"
},
"totalCBD": {
"full": "Total CBD: 0.1 %",
"name": "Total CBD",
"value": 0.1,
"unit": "%"
},
"topTerpene1": {
"full": "Myrcene: 1.2 %",
"name": "Myrcene",
"value": 1.2,
"unit": "%"
}
},
"hasTerpeneResults": true
}How keys are formed
- “Total THC” and “Total Delta-9 THC” both become
totalTHC, and THCa becomesTHCA. - Listed tests use camelCase keys with acronyms kept in capitals, such as
totalCBD,delta9THC,CBG, andbetaMyrcene. - A test that is not on the list is still indexed, under its name in lowercase with everything but letters and digits removed.
- Keys are case-sensitive in JSON, so match them exactly.
The full list of keys, covering cannabinoids, terpenes, residual solvents, and more, lives on the Lab Result Keys reference.
What each entry contains
| Field | Meaning |
|---|---|
full | Name, value, and unit combined, such as “Total THC: 22.4 %” |
name | The test name as the lab printed it |
value | The numeric value when available; may be a string such as “ND” or “<LOQ” for non-detects |
unit | The measurement unit, such as %, mg/g, or ppm |
byUnit | The winning result for each unit the test was reported in, keyed percent, mgPerG, mgPerServing, mgPerPackage, or ppm |
When a test is reported more than once
Each entry holds a single result, chosen in this order: a real result over a lab quality-control figure (RPD), then a % result over any other unit, then a nonzero value over zero, then the most recently performed test. Because byUnit keeps the winner for every unit, a per-serving and a per-package result both stay reachable.
Top terpenes
topTerpene1, topTerpene2, and so on rank the package’s individual terpenes by descending value. Rollups such as Total Terpenes and Other Terpenes are excluded, and byUnit is absent on these entries. hasTerpeneResults is true exactly when topTerpene1 is present, so you can toggle a terpene section without probing for numbered keys.
indexed = package["metadata"].get("indexedLabResults", {})
thc = indexed.get("totalTHC")
if thc:
print(thc["full"]) # "Total THC: 22.4 %"
per_serving = thc and thc.get("byUnit", {}).get("mgPerServing")
if per_serving:
print(per_serving["value"], per_serving["unit"])
if package["metadata"].get("hasTerpeneResults"):
print("Top terpene:", indexed["topTerpene1"]["name"])Lab results in reports
To export lab values for a whole license, use a super report with include=labResults and rowMode=collapsed, which keeps one row per package instead of one row per analyte, then select the metadata columns you need:
GET https://api.trackandtrace.tools/v2/packages/active/super/report
?licenseNumber=LIC-000123
&include=labResults
&rowMode=collapsed
&columns=label,item.name,metadata.indexedLabResults.totalTHC.value,metadata.indexedLabResults.totalCBD.value
&contentType=csvEach include costs one Metrc request per record, and super reports cap at 5,000 rows, so filter to the packages you care about. The export guide covers formats, email delivery, and Spreadsheet Sync.
Print lab values on labels
Compliance labels frequently need potency and terpene values, and T3 label templates read the same keys. Design templates visually in T3 Label Studio, or generate PDFs from your own systems with POST /v2/labels/generate, passing each package object in labelContentDataList. Inside a template, lab values are one expression away:
{{ package.metadata.indexedLabResults.totalTHC.full }}
{{ package.metadata.indexedLabResults.totalCBD.full }}
{% if package.metadata.hasTerpeneResults %}
Terpenes
{{ package.metadata.indexedLabResults.topTerpene1.full }}
{{ package.metadata.indexedLabResults.topTerpene2.full }}
{% endif %}Watermarks and T3+
Related guides
How to export Metrc data
Put lab values into CSV, Excel, or Google Sheets exports with super reports.
Using the Metrc API with Python
Load packages with lab results in Python and paginate full collections.
Metrc API documentation map
Where lab results, labels, and reports sit in the wider API.
Metrc API keys explained
Authenticate to the T3 API with a secret key before pulling results.
The best way to talk to Metrc
The T3 API gives developers one consistent REST interface to Metrc data across every Metrc state, with interactive OpenAPI documentation, predictable JSON, and reports that export full datasets in a single request.
Track & Trace Tools is not affiliated with Metrc.