Loading One Record by ID¶
Every T3 collection endpoint has a matching object endpoint: append the record's id to the collection path and you get that one record back, with no pagination envelope around it.
{
"id": 12345,
"label": "1A4FF0100000022000000123",
"quantity": 4.5,
"unitOfMeasureAbbreviation": "g",
"item": { "name": "Blue Dream Buds", "...": "..." },
"licenseNumber": "LIC-00001",
"dataModel": "MetrcPackage",
"retrievedAt": "2026-09-22T17:42:11.482Z"
}
Compare that to the collection form, which returns the same record wrapped in a page:
{
"data": [ { "id": 12345, "...": "..." } ],
"total": 1,
"page": 1,
"pageSize": 1,
"totalPages": 1
}
An object request is as fast as a collection request, however many records the license has.
Why Use It¶
- You already have the id.
/v2/searchhands back ids, as does every collection response and every report row. The object endpoint is where you spend one. - You want one hydrated record.
/v2/packages/active/super/12345?include=labResultsloads a single package with its lab results attached, far faster than loading includes for a whole supercollection page. - You want a real 404. An id that does not exist is a
404, not a200with an empty list you have to check for.
Request¶
| Part | Notes |
|---|---|
id in the path | The record's Metrc id, an integer. Non-numeric paths are not object lookups, so /v2/packages/active/super is still the supercollection. |
licenseNumber | Required, exactly as on the collection. |
include | Supported on /super paths, with the same values the supercollection accepts. See Supercollection Includes. |
collectionMask | Supported, and shapes the returned record. |
page, pageSize, sort, filter, filterLogic and strictPagination are rejected with a 400. The response is a single record, so there is nothing for them to act on; use the collection endpoint when you want a page.
Responses¶
| Status | Meaning |
|---|---|
200 | The record, as a bare JSON object. |
400 | A paging, sorting or filtering parameter was sent, or an unrecognized include. |
404 | No record with that id exists in this collection for this license. |
A 404 means "not in this collection". A package that has moved to inactive is a 404 on /v2/packages/active/{id} and a 200 on /v2/packages/inactive/{id}. If you do not know which state a record is in, try each in turn, or use /v2/search to find it first.
Example¶
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "t3api-utils",
# ]
# ///
from t3api_utils.api.operations import send_api_request
from t3api_utils.main.utils import get_authenticated_client_or_error, pick_license
def main():
api_client = get_authenticated_client_or_error()
license_number = pick_license(api_client=api_client)["licenseNumber"]
# One package, with its lab results, in a single call.
response = send_api_request(
api_client,
"/v2/packages/active/super/12345",
params={
"licenseNumber": license_number,
"include": "labResults",
},
expected_status=(200, 404),
response_type="response",
)
if response.status_code == 404:
print("No active package with that id in this license")
return
package = response.json()
print(package["label"], package["quantity"], package["unitOfMeasureAbbreviation"])
for result in package["labResults"]:
print(" ", result["testTypeName"], result["testResultLevel"])
if __name__ == "__main__":
main()
Going the other way, from a search hit to the full record: the hit's t3EndpointId names the collection, and its object endpoint shares that id. T3 Endpoint IDs lists every collection's id and path.
hit = send_api_request(
api_client,
"/v2/search",
params={"licenseNumber": license_number, "query": "1A4FF0100000022000000123"},
)["data"][0]
assert hit["t3EndpointId"] == "METRC_ACTIVE_PACKAGES" # /v2/packages/active
package = send_api_request(
api_client,
f"/v2/packages/active/{hit['matchedEntry']['id']}",
params={"licenseNumber": license_number},
)
Every Object Endpoint¶
| Collection | Object | Super object |
|---|---|---|
| Active Packages | /v2/packages/active/{id} | /v2/packages/active/super/{id} |
| Inactive Packages | /v2/packages/inactive/{id} | /v2/packages/inactive/super/{id} |
| In Transit Packages | /v2/packages/intransit/{id} | /v2/packages/intransit/super/{id} |
| On Hold Packages | /v2/packages/onhold/{id} | /v2/packages/onhold/super/{id} |
| Transferred Packages | /v2/packages/transferred/{id} | /v2/packages/transferred/super/{id} |
| Product Labels | /v2/packages/product-labels/{id} | /v2/packages/product-labels/super/{id} |
| Staged Product Labels | /v2/packages/staged-product-labels/{id} | /v2/packages/staged-product-labels/super/{id} |
| Lab Samples | /v2/packages/lab-samples/{id} | /v2/packages/lab-samples/super/{id} |
| Items | /v2/items/{id} | /v2/items/super/{id} |
| Strains | /v2/strains/{id} | /v2/strains/super/{id} |
| Active Locations | /v2/locations/active/{id} | /v2/locations/active/super/{id} |
| Active Harvests | /v2/harvests/active/{id} | /v2/harvests/active/super/{id} |
| Inactive Harvests | /v2/harvests/inactive/{id} | /v2/harvests/inactive/super/{id} |
| On Hold Harvests | /v2/harvests/onhold/{id} | /v2/harvests/onhold/super/{id} |
| Harvest Schedules | /v2/harvests/schedules/{id} | (none) |
| Vegetative Plants | /v2/plants/vegetative/{id} | /v2/plants/vegetative/super/{id} |
| Flowering Plants | /v2/plants/flowering/{id} | /v2/plants/flowering/super/{id} |
| On Hold Plants | /v2/plants/onhold/{id} | /v2/plants/onhold/super/{id} |
| Inactive Plants | /v2/plants/inactive/{id} | /v2/plants/inactive/super/{id} |
| Active Mother Plants | /v2/plants/mother/active/{id} | /v2/plants/mother/active/super/{id} |
| On Hold Mother Plants | /v2/plants/mother/onhold/{id} | /v2/plants/mother/onhold/super/{id} |
| Inactive Mother Plants | /v2/plants/mother/inactive/{id} | /v2/plants/mother/inactive/super/{id} |
| Plant Waste | /v2/plants/waste/{id} | /v2/plants/waste/super/{id} |
| Plant Additives | /v2/plants/additives/{id} | /v2/plants/additives/super/{id} |
| On Hold Plant Batches | /v2/plantbatches/onhold/{id} | /v2/plantbatches/onhold/super/{id} |
| Active Sales | /v2/sales/active/{id} | /v2/sales/active/super/{id} |
| Inactive Sales | /v2/sales/inactive/{id} | /v2/sales/inactive/super/{id} |
| Available Tags | /v2/tags/available/{id} | /v2/tags/available/super/{id} |
| Used Tags | /v2/tags/used/{id} | /v2/tags/used/super/{id} |
| Voided Tags | /v2/tags/voided/{id} | /v2/tags/voided/super/{id} |
| Current Tag Orders | /v2/tagorders/current/{id} | /v2/tagorders/current/super/{id} |
| Historical Tag Orders | /v2/tagorders/history/{id} | /v2/tagorders/history/super/{id} |
| Active Processing Jobs | /v2/processingjobs/active/{id} | /v2/processingjobs/active/super/{id} |
| Inactive Processing Jobs | /v2/processingjobs/inactive/{id} | /v2/processingjobs/inactive/super/{id} |
| Incoming Active Transfers | /v2/transfers/incoming/active/{id} | /v2/transfers/incoming/active/super/{id} |
| Incoming Inactive Transfers | /v2/transfers/incoming/inactive/{id} | /v2/transfers/incoming/inactive/super/{id} |
| Outgoing Active Transfers | /v2/transfers/outgoing/active/{id} | /v2/transfers/outgoing/active/super/{id} |
| Outgoing Inactive Transfers | /v2/transfers/outgoing/inactive/{id} | /v2/transfers/outgoing/inactive/super/{id} |
| Rejected Transfers | /v2/transfers/rejected/{id} | /v2/transfers/rejected/super/{id} |
| Hub Transfers | /v2/transfers/hub/{id} | /v2/transfers/hub/super/{id} |
| Transfer Templates | /v2/transfers/templates/{id} | (none) |
The eligibility lookups that feed each write follow the same rule: every source-* collection, such as /v2/packages/create/source-packages or /v2/plants/destroy/source-plants, also answers /{id} with one record. The interactive docs list every one.
Four Exceptions¶
Plant batches. /v2/plantbatches/active and /v2/plantbatches/inactive have no object endpoint, because those two collections cannot be filtered by id. On hold plant batches are unaffected. To find one active plant batch, page /v2/plantbatches/active and match on id yourself, or filter by name.
Data import templates. /v2/dataimport/templates has no object endpoint, because Metrc ignores an id filter on that collection and returns every template. Page /v2/dataimport/templates and match on id yourself.
Transferred packages. On /v2/packages/transferred, a row's id is the id of the manifest it travelled on, shared by every package on that manifest. The id in the path is therefore the packageId, which is the package's own identity. A package transferred more than once has one row per manifest; the object endpoint returns the first. Use /v2/packages/transferred?filter=packageId__eq:12345 when you need all of them.
Lab samples. /v2/packages/lab-samples lists a sample once per test, and every row carries the sample's package id. The id in the path is therefore the row's labTestDetailId, which names one test on one sample. Use /v2/packages/lab-samples?filter=id__eq:12345 for every test on a sample.
Sub-Collections Are Different¶
Endpoints that return records belonging to a parent record already take that parent's id as a query parameter, and keep doing so:
GET /v2/packages/history?packageId=12345&licenseNumber=LIC-00001
GET /v2/items/notes?itemId=987&licenseNumber=LIC-00001
GET /v2/transfers/deliveries?manifestNumber=0000123456&licenseNumber=LIC-00001
These have no /{id} form: the id they need is the parent's, not the row's.
Next Steps¶
- Find a record when you do not have its id: Search.
- Attach related records to the one you loaded: Supercollections and Supercollection Includes.
- Load many records at once: Reports.