Skip to content

Delivery Examples

Recipes for cutting a report down and sending it somewhere, a filtered export, an emailed Excel workbook, a live Google Sheet.

Each uses the same preamble as the first script in Report Examples: a uv script header, imports, and your SECRET_KEY and LICENSE_NUMBER. Scripts authenticate with the X-T3-API-Key header; the plain URLs carry ?secretKey= because they are meant to be pasted somewhere.

Narrowing a report with filters, columns and a row limit

Reports return everything by default, which is rarely what you want while you are still building the request. These three parameters work together to cut a report down:

  • filter narrows which records come back. Repeat it for multiple conditions.
  • columns picks the columns, in the order you list them.
  • rowLimit caps how many records are loaded.

Say you want the label and quantity of every active package in the Bulk Storage room holding more than 100 units:

https://api.trackandtrace.tools/v2/packages/active/report?secretKey=YOUR_SECRET_KEY&licenseNumber=EX-00001&filter=locationName__eq:Bulk Storage&filter=quantity__gte:100&filterLogic=and&columns=label,item.name,quantity,unitOfMeasureAbbreviation&sort=quantity:desc&rowLimit=10
# 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/report",
        params={
            "licenseNumber": LICENSE_NUMBER,
            "filter": [
                "locationName__eq:Bulk Storage",
                "quantity__gte:100",
            ],
            "filterLogic": "and",
            "columns": "label,item.name,quantity,unitOfMeasureAbbreviation",
            "sort": "quantity:desc",
            # Start small while you get the filters right, then remove this.
            "rowLimit": 10,
            "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()

    print(f"{len(report['data'])} packages")

    for package in report["data"]:
        print(package["label"], package["quantity"])

if __name__ == "__main__":
    main()

Build with a small rowLimit, then remove it

rowLimit=10 returns in a second or two and produces the exact same columns and formatting the full report will, enough to confirm your filter matches what you expect and that your columns are spelled correctly, without pulling your whole dataset on every attempt.

Note that sort matters once rowLimit is in play: only the first n records are loaded, so sort=quantity:desc&rowLimit=10 gives you the ten largest packages, not ten arbitrary ones.

The column names available for a given report come from its dataModel, which is listed in the metadata preamble and documented under Schemas in the API docs.

Emailing a large report as an Excel workbook

The problem: your license is big enough that the report times out. A normal request waits 240 seconds and then returns Report Generation Timeout, and retrying does not help.

&delivery=email solves this, because nothing is waiting on the result; the report is built however long it takes and then sent to you. Pair it with &contentType=xlsx to get an Excel workbook attached to the email:

https://api.trackandtrace.tools/v2/packages/active/report?secretKey=YOUR_SECRET_KEY&licenseNumber=EX-00001&contentType=xlsx&delivery=email&[email protected]
# 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/report",
        params={
            "licenseNumber": LICENSE_NUMBER,
            "contentType": "xlsx",
            "delivery": "email",
            "email": "[email protected]",
        },
        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()

    # You get a receipt immediately -- the report itself arrives by email.
    print(response.json())

if __name__ == "__main__":
    main()

The response comes back right away, before the report exists:

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

There is nothing to poll after that. The workbook arrives by email, or (if something goes wrong during generation) an email explaining what went wrong arrives instead, with the same detail an ordinary request would have returned.

Your parameters are still checked before the receipt is sent, so a misspelled columns or a malformed email still comes back as an immediate 400.

Never put delivery=email on a Spreadsheet Sync link

Sync links are refreshed on a schedule, so you would get an email on every refresh rather than a one-off export.

Delivering a report to Google Sheets

&contentType=googleSheets builds a Google Sheet. Add &delivery=email and the link is emailed to the account you name instead of returned, useful for a report too large to finish inside a request. (Without delivery=email you get a 302 straight to the sheet; see The googleSheets Format.)

https://api.trackandtrace.tools/v2/packages/active/report?secretKey=YOUR_SECRET_KEY&licenseNumber=EX-00001&contentType=googleSheets&delivery=email&[email protected]
# Same preamble as the first example: uv script header,
# imports, SECRET_KEY and LICENSE_NUMBER.

# Must be a Google account -- the sheet is shared with it directly.
GOOGLE_ACCOUNT = "[email protected]"

def main():
    response = httpx.get(
        "https://api.trackandtrace.tools/v2/packages/active/report",
        params={
            "licenseNumber": LICENSE_NUMBER,
            "contentType": "googleSheets",
            "delivery": "email",
            "email": GOOGLE_ACCOUNT,
            # Without this, anyone with the sheet's link can open and edit it.
            "sheetVisibility": "private",
        },
        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()

    # The sheet link is emailed once the sheet has been created and shared.
    print(response.json())

if __name__ == "__main__":
    main()

You get the same processing receipt as any other emailed report, it carries no sheet URL, because the sheet does not exist yet. Once it does, you receive the link twice: once from T3 and once from Google's own share notification.

Three things to know:

  • The recipient 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 get an email explaining why. Use contentType=xlsx to receive a file instead.
  • Sheets are public by default, here too. Omit sheetVisibility and anyone with the link can open and edit the sheet, not just the recipient. Pass sheetVisibility=private to restrict it, as the example above does.
  • Every request creates a new sheet, and sheets are never deleted. Use csv or xlsx for anything recurring, a job that pulls googleSheets every five minutes leaves behind an abandoned sheet every five minutes.

This is not the same as the older Exports (Legacy) tool in the Chrome Extension, which signs into your Google account and creates the sheet there. T3 Reports uses this endpoint, so a sheet it creates behaves exactly as described here.

Next Steps