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},
        # A report waits up to 240 seconds before giving up.
        timeout=250.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['label']}")

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",
            # Manifest reports reach the size limit early. See the note below.
            "rowLimit": 500,
        },
        headers={"X-T3-API-Key": SECRET_KEY},
        # A report waits up to 240 seconds before giving up.
        timeout=250.0,
    )
    response.raise_for_status()
    report = response.json()

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

if __name__ == "__main__":
    main()

In JSON, each record is a transfer with its packages and transporters nested inside it. In csv, xlsx and googleSheets, each row is stitched together from the transfer, a package and the transporter, so 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

rowLimit counts transfers, not rows, and these reports reach the size limit at a few thousand transfers. See Transfer manifest reports work differently.

/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.

Received transfer manifests

The problem: you want the same line-item list for transfers you have already received, over a date range: who shipped each package, what it was, how much, when it was due, and when it arrived.

A received transfer leaves the incoming list, so use /v2/transfers/incoming/inactive/manifest/report and filter on receivedDateTime, written as a full UTC timestamp:

https://api.trackandtrace.tools/v2/transfers/incoming/inactive/manifest/report?secretKey=YOUR_SECRET_KEY&licenseNumber=EX-00001&contentType=googleSheets&filter=receivedDateTime__gte:2026-08-01T00:00:00.000Z&sort=receivedDateTime:asc&columns=transfer.manifestNumber,transfer.shipperFacilityName,transfer.estimatedArrivalDateTime,transfer.receivedDateTime,package.packageLabel,package.productName,package.shippedQuantity,package.shippedUnitOfMeasureAbbreviation
# 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/inactive/manifest/report",
        params={
            "licenseNumber": LICENSE_NUMBER,
            "contentType": "json",
            "filter": "receivedDateTime__gte:2026-08-01T00:00:00.000Z",
            "sort": "receivedDateTime:asc",
        },
        headers={"X-T3-API-Key": SECRET_KEY},
        timeout=250.0,
    )
    response.raise_for_status()

    for transfer in response.json()["data"]:
        for package in transfer.get("packages", []):
            print(
                transfer["receivedDateTime"],
                transfer["shipperFacilityName"],
                package["productName"],
                package["shippedQuantity"],
                package["shippedUnitOfMeasureAbbreviation"],
            )

if __name__ == "__main__":
    main()

The default columns are the incoming manifest report's plus transfer.receivedDateTime. To narrow to one shipper, add &filter=shipperFacilityInfo__contains:Example Farms. filterLogic applies to every filter at once, so a date filter cannot be combined with an or across several shippers: run one report per shipper instead.

/v2/transfers/outgoing/inactive/manifest/report is the outgoing equivalent. Filter it on createdDateTime, since each delivery of an outgoing transfer is received separately; its default columns add delivery.receivedDateTime.

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 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

A scan sheet loads the same data as the report it is built from, so the two share a cached result: run the report, then the scan sheet, and the scan sheet is served from cache and returns immediately.

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, so narrow to what you need.
            "filter": "item.productCategoryName__eq:Buds",
        },
        headers={"X-T3-API-Key": SECRET_KEY},
        # A report waits up to 240 seconds before giving up.
        timeout=250.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 adds work for every 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