Skip to content

Delivering a Report

Emailing a Report

Add &delivery=email&[email protected] and the report is sent to that address instead of returned. The two parameters are required together, one without the other is a 400. (The exception is contentType=googleSheets, where email on its own names the account to share the sheet with; see below.)

You get back a receipt straight away, before the report has been built:

{
  "status": "processing",
  "recipient": "[email protected]",
  "contentType": "xlsx"
}

The only way past the 240-second ceiling

A normal request gives up at that point and returns a 504, and retrying will not help; the report is simply not obtainable over HTTP. With delivery=email nothing is waiting on the result, so it is built and delivered however long it takes.

json, csv and xlsx arrive as a file attached to an email. googleSheets creates a sheet, shares it with that address, and emails you the link. Google sends its own share notification too, so you get two messages carrying the same link.

Errors arrive by email too

Once you have the receipt there is no response left to fail into, so anything that goes wrong afterwards (Report Request Too Large, a Metrc permission problem, a report too big to attach) is emailed to the recipient with the same explanation an ordinary request would have returned. Your parameters are still checked before the receipt is sent, so a bad columns, rowLimit, include or email still comes back as an immediate 400.

A few things to know:

  • One recipient per request; a comma-separated list is rejected.
  • Email delivery has its own rate limit, tighter than the report limits.
  • There is nothing to poll. The report arrives, or an explanation does.
  • On a Spreadsheet Sync link, every refresh sends an email. Sync links are polled on a schedule, so delivery=email on one produces a message per refresh rather than a one-off export.

The googleSheets Format

contentType=googleSheets writes the report into a new Google Sheet. It is the one format whose response is not the report; it is a way to reach the document.

Without delivery=email, you get a redirect. The sheet is built during the request, and the response is a 302 pointing at it:

curl -i "https://api.trackandtrace.tools/v2/packages/active/report?secretKey=YOUR_SECRET_KEY&licenseNumber=EX-00001&contentType=googleSheets"
HTTP/1.1 302 Found
Location: https://docs.google.com/spreadsheets/d/1AbCdEf.../edit
Content-Type: application/json

{"sheetUrl": "https://docs.google.com/spreadsheets/d/1AbCdEf.../edit", "sheetId": "1AbCdEf..."}

Paste that URL into a browser and you land on the finished sheet. The link is repeated in the body so that a client which does not follow redirects can still read it:

# Follow the redirect and print where you ended up
curl -sL -o /dev/null -w '%{url_effective}\n' \
  "https://api.trackandtrace.tools/v2/packages/active/report?secretKey=YOUR_SECRET_KEY&licenseNumber=EX-00001&contentType=googleSheets"
import requests

response = requests.get(
    "https://api.trackandtrace.tools/v2/packages/active/report",
    params={
        "licenseNumber": "EX-00001",
        "contentType": "googleSheets",
    },
    headers={"Authorization": f"Bearer {access_token}"},
    allow_redirects=False,
)

print(response.json()["sheetUrl"])

Anyone with the link can open and edit the sheet

There is no Google account to grant access to on a plain request, so the URL is the credential. Treat it the way you would treat the report itself; it holds your Metrc data.

To restrict it, add &sheetVisibility=private together with an email:

https://api.trackandtrace.tools/v2/packages/active/report?secretKey=YOUR_SECRET_KEY&licenseNumber=EX-00001&contentType=googleSheets&sheetVisibility=private&[email protected]

Now the sheet is shared only with that Google account. You still get the 302, but you will only land on the sheet if your browser is signed into that account, anyone else sees Google's request-access page. sheetVisibility=private without an email is a 400, because the sheet would open for nobody.

sheetVisibility works the same way with delivery=email, and defaults to public there too. Add &sheetVisibility=private if you want the emailed sheet restricted to its recipient.

Three more things to know:

  • A named email must be a Google account. The sheet is granted to that address directly, and Google Drive may refuse an address it does not recognize. If it does, no sheet is left behind and you are told why. Use contentType=xlsx to receive a file instead.
  • Use delivery=email for large reports. Without it, the sheet is created and filled inside your request, on top of generating the report, a report near the row cap can run out of time partway through. With delivery=email nothing is waiting, so there is no ceiling.
  • Every request creates a new sheet, and sheets are never deleted. Use csv for anything recurring, a job that pulls googleSheets every five minutes leaves behind an abandoned sheet every five minutes.

Saving a report

Once you have a report URL configured the way you want it, you can save it under a name so you can find it again, and so a future release can run it on a schedule for you.

A saved report is a definition, not a shortcut. Saving one does not run it: you save the URL, and later you retrieve it and request it exactly as you would have anyway.

Saving one

POST the URL you already have. Both a full URL and a bare path work:

# /// script
# requires-python = ">=3.8"
# dependencies = [
#     "httpx",
# ]
# ///
import httpx

SECRET_KEY = "YOUR_SECRET_KEY"

response = httpx.post(
    "https://api.trackandtrace.tools/v2/saved-reports",
    headers={"X-T3-API-Key": SECRET_KEY},
    json={
        "name": "Daily veg plant count",
        "description": "Feeds the cultivation team's morning dashboard",
        "reportUrl": (
            "/v2/plants/vegetative/report"
            "?licenseNumber=EX-00001"
            "&contentType=csv"
            "&columns=label,strainName,plantedDate"
            "&prependCsvMetadata=false"
        ),
    },
)

saved = response.json()["data"]
print(saved["publicId"])
print(saved["reportUrl"])

Your secret key is never stored. If you paste a URL that still has &secretKey=... on the end (which is what you get by copying it out of your browser) it is stripped before the report is saved. A saved report is not a credential.

The URL is checked when you save it. An unknown filter field, a bad contentType, a missing licenseNumber: all of these fail here, while you are looking at them, rather than the next time you go to use the report.

Using one

Retrieve it and request the reportUrl it gives you back.

Two things are yours to add. reportUrl is a path, not a full address, and it carries no credential:

  • Prepend the API origin: https://api.trackandtrace.tools. The origin is left off on purpose, so one saved report is correct whatever host you read it from.
  • Supply your secret key: as ?secretKey= or an X-T3-API-Key header. It was stripped when you saved, and is never stored.
saved = httpx.get(
    f"https://api.trackandtrace.tools/v2/saved-reports/{public_id}",
    headers={"X-T3-API-Key": SECRET_KEY},
).json()["data"]

report = httpx.get(
    f"https://api.trackandtrace.tools{saved['reportUrl']}",
    params={"secretKey": SECRET_KEY},
)

The reportUrl you get back is not byte-identical to what you sent. The host is dropped, your secret key is removed, licenses are tidied up, and the query string is re-encoded. It is the same request, spelled canonically.

To paste one into a browser, an IMPORTDATA() formula or Power Query, join the three parts yourself:

https://api.trackandtrace.tools  +  reportUrl  +  &secretKey=YOUR_SECRET_KEY

One useful consequence: the URL is rebuilt from the endpoint each time you read it, so a report saved against a path that later gets corrected comes back pointing at the current path.

Managing them

Method Path What it does
GET /v2/saved-reports List your saved reports. Filter with ?licenseNumber= or ?t3EndpointId=
POST /v2/saved-reports Save a report URL
GET /v2/saved-reports/{publicId} Retrieve one
PATCH /v2/saved-reports/{publicId} Change the name, description or URL
DELETE /v2/saved-reports/{publicId} Delete it permanently

Saved reports belong to your Metrc account, not to a single secret key. Anyone signing in as the same username on the same state's Metrc sees the same list, and rotating or revoking a secret key does not affect them.

Next Steps