Using the Metrc API with Python

Python is a natural fit for Metrc automation: scheduled exports, inventory reconciliation, and data pipelines that feed other systems. This guide shows how to call Metrc data from Python through the T3 API using nothing but requests, then how to skip the boilerplate with the T3 Python libraries.

Updated

What you need

  • Python 3 and requests. Install it with pip install requests.
  • A T3 secret key. The Metrc API key guide explains how to generate one. The examples below read it from a T3_API_KEY environment variable so it never lands in source control.
  • A license number. Nearly every data endpoint requires a licenseNumber query parameter.
  • T3+ for most endpoints. Authentication, licenses, and a few other endpoints are free; the rest require a T3+ subscription.

Make your first request

The script below fetches active packages for one license by calling GET /v2/packages/active with the secret key in the X-T3-API-Key header. Replace LIC-000123 with one of your own license numbers.

first_request.py
import os
import requests

API_BASE = "https://api.trackandtrace.tools"
HEADERS = {"X-T3-API-Key": os.environ["T3_API_KEY"]}

response = requests.get(
    f"{API_BASE}/v2/packages/active",
    params={"licenseNumber": "LIC-000123"},
    headers=HEADERS,
    timeout=60,
)
response.raise_for_status()
body = response.json()

print(f"{body['total']} active packages")
for package in body["data"]:
    print(
        package["label"],
        package["item"]["name"],
        package["quantity"],
        package["unitOfMeasureAbbreviation"],
    )

Every collection response has the same envelope: data holds the records, and page, pageSize, and total describe where you are in the collection. Each package carries fields such as label, quantity, labTestingStateName, and a nested item object.

Look up your license numbers

GET /v2/licenses returns every license your Metrc user can access, as a list of licenseNumber and licenseName pairs. It does not require T3+.

python
licenses = requests.get(
    f"{API_BASE}/v2/licenses", headers=HEADERS, timeout=60
).json()

for license in licenses:
    print(license["licenseNumber"], license["licenseName"])

Paginate through a full collection

Collections are paginated. page is 1-indexed, pageSize defaults to 100, and most endpoints cap it at 500, with lower caps on supercollections. Requests above the cap are trimmed rather than rejected, so the reliable stopping condition is comparing what you have loaded against total:

python
def load_all(path, license_number, page_size=500, **params):
    """Load every page of a T3 API collection into one list."""
    records = []
    page = 1
    while True:
        response = requests.get(
            f"{API_BASE}{path}",
            params={
                "licenseNumber": license_number,
                "page": page,
                "pageSize": page_size,
                **params,
            },
            headers=HEADERS,
            timeout=60,
        )
        response.raise_for_status()
        body = response.json()
        records.extend(body["data"])

        if not body["data"] or len(records) >= body["total"]:
            return records
        page += 1


packages = load_all("/v2/packages/active", "LIC-000123")
print(len(packages), "packages loaded")

Filter, sort, and trim on the server

Large licenses can hold thousands of packages, so narrow the result before it reaches Python:

  • filter uses fieldName__operator:value, such as quantity__gte:1000. Repeat it for multiple conditions, and add filterLogic=or to match any of them instead of all.
  • sort takes one root-level field and a direction, such as packagedDate:desc.
  • collectionMask returns only the fields you list, using dot notation for nested values, which keeps large pages small.
python
packages = load_all(
    "/v2/packages/active",
    "LIC-000123",
    filter=["quantity__gte:1000", "labTestingStateName__eq:TestPassed"],
    sort="packagedDate:desc",
    collectionMask="label,item.name,quantity,unitOfMeasureAbbreviation",
)

requests sends a list value as repeated filter parameters, which is exactly the syntax the API expects.

Handle errors and rate limits

Error responses follow the RFC 7807 Problem Details format, with a machine-readable code and a type URL that points to the error reference. Log the response body rather than just the status code. The default rate limit is 600 requests per minute per user, and some routes are lower, so add a pause or retry with backoff in tight loops.

Pull related data with supercollections

Add /super to a collection path to attach related records with the include parameter. For packages, include accepts labResults, labResultBatches, sourceHarvests, and history. Each record also gains a metadata object with derived values, such as normalized lab results:

python
packages = load_all(
    "/v2/packages/active/super",
    "LIC-000123",
    include=["labResults"],
)

for package in packages:
    thc = package["metadata"].get("indexedLabResults", {}).get("totalTHC")
    if thc:
        print(package["label"], thc["full"])  # e.g. "Total THC: 22.4 %"

The lab results guide covers those normalized keys in detail.

Writing data back to Metrc

Reading is only half of most workflows. Write endpoints such as POST /v2/packages/create, POST /v2/transfers/create, and POST /v2/plants/changegrowthphase accept JSON bodies sent with requests.post(..., json=payload). Before building a payload, call the matching inputs endpoint, such as GET /v2/transfers/create/inputs, which returns the facilities, transfer types, units of measure, drivers, and vehicles your request needs to reference. Building payloads from that reference data instead of hard-coded IDs keeps scripts working as your Metrc setup changes.

If your Metrc account is provisioned for a sandbox, POST /v2/auth/sandbox exchanges your session for a token scoped to the matching sandbox host, so you can rehearse writes before running them against live data.

Skip the boilerplate with T3 Scripts

T3 Scripts are Python scripts built on the T3 Python libraries. They use uv inline dependency metadata, so a single file declares what it needs and uv run script.py installs it in an isolated environment. The script below replaces the hand-written authentication and pagination code above:

load_packages.py
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.8"
# dependencies = [
#     "t3api_utils",
# ]
# ///

from t3api_utils.api.parallel import load_all_data_sync
from t3api_utils.main.utils import (
    get_authenticated_client_or_error,
    interactive_collection_handler,
    pick_license,
)

api_client = get_authenticated_client_or_error()

license = pick_license(api_client=api_client)

all_packages = load_all_data_sync(
    client=api_client,
    path="/v2/packages/active",
    license_number=license["licenseNumber"],
)

interactive_collection_handler(data=all_packages)

get_authenticated_client_or_error walks you through authenticating with credentials, a JWT, or a secret key. pick_license prompts for a license, load_all_data_sync loads the entire collection in parallel, and interactive_collection_handler opens a menu for saving the results to files, loading them into a database, or exporting their schema.

The T3 Python libraries

  • t3api is a wrapper around the entire T3 API, so you can authenticate and send requests in a structured way without building HTTP calls by hand.
  • t3api-utils is a collection of pre-built utilities for common patterns, including automatic authentication, license selection, and loading entire collections in parallel.
  • t3py is a command line program with prebuilt commands, run as t3py [<command_name>].

Environment setup is covered in the Setting Up Python guide, and more complete programs live in the T3 API examples repository.

When a report beats a loop

Pagination is the right tool for applications that work through records. If you only need a complete dataset, report endpoints load the whole collection in a single request and return it as JSON, CSV, an Excel workbook, or a new Google Sheet. The Metrc export guide shows how to shape those reports.

Going further

Everything in this guide works the same way across every Metrc state. Browse the full set of T3 API capabilities to see what else you can automate from Python, including transfers, labels, and plant workflows.

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.