Skip to content

Report Examples

Every example below uses your secret key for authentication, which is the simplest way to call a report endpoint from a script. The scripts are written for uv, save one as example.py and run it with uv run example.py.

Each example also shows the plain URL. A URL carries the key as ?secretKey= rather than a header because it is meant to be pasted somewhere, a browser, an IMPORTDATA() formula, Power Query. In a script, prefer the X-T3-API-Key header, as every script below does.

Packages from each harvest

The problem: you want to know which packages came out of each harvest. Metrc keeps harvests and packages in separate places, so ordinarily you export a harvest report, export a package report, and try to match them up by hand.

A super report does the join for you. /v2/harvests/active/super/report returns your harvests, and &include=packages attaches the packages created from each one:

https://api.trackandtrace.tools/v2/harvests/active/super/report?secretKey=YOUR_SECRET_KEY&licenseNumber=EX-00001&include=packages&contentType=json
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.8"
# dependencies = [
#     "httpx",
# ]
# ///

import httpx

SECRET_KEY = "1234-1234-1234-1234-1234-1234"
LICENSE_NUMBER = "EX-00001"

def main():
    response = httpx.get(
        "https://api.trackandtrace.tools/v2/harvests/active/super/report",
        params={
            "licenseNumber": LICENSE_NUMBER,
            "include": "packages",
            "contentType": "json",
        },
        headers={"X-T3-API-Key": SECRET_KEY},
        # httpx defaults to 5s; a report may take up to 60.
        timeout=90.0,
    )
    response.raise_for_status()
    report = response.json()

    for harvest in report["data"]:
        packages = harvest.get("packages", [])
        print(f"{harvest['name']}: {len(packages)} packages")

        for package in packages:
            print(f"    {package['packageLabel']}")

if __name__ == "__main__":
    main()

Each harvest in data carries all the usual harvest fields (name, harvestStartDate, plantCount, currentWeight, totalWetWeight, and so on) plus a packages array holding the packages Metrc recorded against it.

In a spreadsheet. Switch to &contentType=csv and the packages are flattened into the rows; you get one row per package, with the harvest columns repeated. That is exactly the shape you want for a pivot table summarizing yield by harvest.

Swap active for inactive or onhold to run the same report against harvests in those states. include=plants attaches the plants that went into each harvest instead, and include=history attaches the harvest's Metrc history.

Incoming transfer manifests

The problem: you want a line-item list of everything arriving at your facility, not one row per transfer, but one row per package on each transfer, with the manifest number and transporter attached.

/v2/transfers/incoming/manifest/report does this in a single request. Unlike a super report, you do not ask for the packages with include, this report always loads the packages and transporters for every transfer and cross-joins them:

https://api.trackandtrace.tools/v2/transfers/incoming/manifest/report?secretKey=YOUR_SECRET_KEY&licenseNumber=EX-00001&contentType=csv
# Same preamble as the first example: uv script header,
# imports, SECRET_KEY and LICENSE_NUMBER.

def main():
    response = httpx.get(
        "https://api.trackandtrace.tools/v2/transfers/incoming/manifest/report",
        params={
            "licenseNumber": LICENSE_NUMBER,
            "contentType": "json",
            # These reports get expensive quickly -- see the note below.
            "rowLimit": 500,
        },
        headers={"X-T3-API-Key": SECRET_KEY},
        # httpx defaults to 5s; a report may take up to 60.
        timeout=90.0,
    )
    response.raise_for_status()
    report = response.json()

    for row in report["data"]:
        print(
            row["transfer"]["manifestNumber"],
            row["package"]["packageLabel"],
            row["package"]["productName"],
            row["package"]["shippedQuantity"],
            row["package"]["shippedUnitOfMeasureAbbreviation"],
        )

if __name__ == "__main__":
    main()

Because each row is stitched together from three different Metrc objects, the columns are prefixed by where they came from. The default columns are:

Column Comes from
transfer.manifestNumber The transfer
transfer.recipientFacilityLicenseNumber The transfer
transfer.recipientFacilityName The transfer
transporter.transporterFacilityName The transporter
transporter.transporterFacilityLicenseNumber The transporter
package.packageLabel The package
package.productName The package
package.shippedQuantity The package
package.shippedUnitOfMeasureAbbreviation The package

Two things surprise people about these reports:

  • rowLimit counts transfers, not rows. It is applied before the cross join, so &rowLimit=100 can return several hundred rows.
  • They are expensive despite being plain reports. Loading packages and transporters for every transfer costs a Metrc request per transfer, so these run out of the Metrc request budget at roughly 5,000 transfers, well before the 50,000 row cap. Filter to a date range or add &rowLimit=5000.

/v2/transfers/outgoing/manifest/report is the same report for outgoing transfers, and adds driver and vehicle columns (transporterDetails.driverName, transporterDetails.vehicleLicensePlateNumber, and so on). /v2/transfers/rejected/manifest/report is the rejected equivalent, with identical columns; Metrc returns rejected transfers in the same shape as outgoing ones.

Reconciling a shipment as it is unloaded

The problem: a truck has arrived and someone has to check that every package on the manifest is physically present, and that nothing turned up that should not have.

Add &transform=scanSheet to any of the four manifest reports and you get a spreadsheet built for exactly that: one row per package, a column holding the expected tag, and an empty column beside it that colours itself as a worker scans.

https://api.trackandtrace.tools/v2/transfers/incoming/manifest/report?secretKey=YOUR_SECRET_KEY&licenseNumber=EX-00001&transform=scanSheet&contentType=googleSheets&filter=manifestNumber__eq:0000123456

Because a scan sheet loads the same data as the report it is built from, the two share a cached result, running the report and then the scan sheet costs one trip to Metrc, not two.

See Scan Sheets for the columns, the colour meanings, and the manifest-number filter syntax.

Lab results for every active package

The problem: you want a COA summary across your inventory (every active package with its potency results) without opening each package in Metrc one at a time.

This is the most commonly used super report. &include=labResults attaches the lab results to each package:

https://api.trackandtrace.tools/v2/packages/active/super/report?secretKey=YOUR_SECRET_KEY&licenseNumber=EX-00001&include=labResults&contentType=json
# Same preamble as the first example: uv script header,
# imports, SECRET_KEY and LICENSE_NUMBER.

def main():
    response = httpx.get(
        "https://api.trackandtrace.tools/v2/packages/active/super/report",
        params={
            "licenseNumber": LICENSE_NUMBER,
            "include": "labResults",
            "contentType": "json",
            # Super reports cap at 5,000 records -- narrow to what you need.
            "filter": "item.productCategoryName__eq:Buds",
        },
        headers={"X-T3-API-Key": SECRET_KEY},
        # httpx defaults to 5s; a report may take up to 60.
        timeout=90.0,
    )
    response.raise_for_status()
    report = response.json()

    for package in report["data"]:
        extracted = package["metadata"]["extractedLabResults"]
        print(package["label"], package["item"]["name"], extracted)

if __name__ == "__main__":
    main()

Each package carries its standard fields plus a labResults array of raw Metrc lab result data, and a metadata object with the results already parsed into a usable shape, extractedLabResults, indexedLabResults, testSamplePackageLabels and labResultPdfs. The metadata fields are usually what you want; the raw labResults array is there when you need something the parsing didn't surface.

In a spreadsheet, add &rowMode=collapsed. Because the metadata fields are already one-per-package, a CSV of this report would otherwise repeat each package once per lab result, a package carries a result per analyte per test, so this is the difference between one row and a hundred. See One row per record, or one row per included record.

Watch the row cap

A super report returns at most 5,000 records, a tenth of a plain report, because each include costs one Metrc request per package. Filtering to a product category, a location, or a date range is usually how you get under it. See Report Request Too Large if you hit the limit.

Other useful package includes are sourceHarvests (which harvest the package came from), history (the package's full Metrc history), and labResultBatches (results grouped by test batch). All the options are listed on the Supercollections page, and published as an enum on each endpoint in the API documentation.

Next Steps