Permissions
- Permissions
- Why Permissions Vary
- Views and Permissions
- Listing Views
- Listing Permissions Within a View
- The
view:actionFormat - Checking Before You Act
- Limits and Behavior
- Examples
- Next Steps
Permissions¶
Two Metrc accounts pointed at the same license rarely see the same thing. The permissions endpoints tell you what your account can actually do, so you can check before you call rather than discovering it from a 403.
A T3+ subscription is not required to use these endpoints.
Why Permissions Vary¶
Metrc access is determined by several independent factors:
- Permissions granted by your Metrc admin. Employee-level grants, set inside Metrc.
- Class of license. A cultivator sees plant pages a manufacturer does not.
- US state. Each state runs its own Metrc deployment with its own enabled modules.
The practical consequence: you cannot hardcode a permission list. A script that works for one license may not work for the next one, even under the same login. Query the permissions instead.
Views and Permissions¶
T3 describes Metrc access at two granularities.
| What it is | Example | Endpoint | |
|---|---|---|---|
| View | A page you can open | packages | GET /v2/permissions/views |
| Permission | An action you can take on that page | packages:remediate | GET /v2/permissions |
A view is a Metrc page path, and it may itself contain a slash — transfers/licensed and admin/items are both single views. A permission is a view and an action joined by a colon.
Both endpoints return a flat JSON array of strings. There is no wrapper object.
Both are scoped to one license, via the required licenseNumber query parameter. Permissions belong to the combination of who you authenticated as and which license you asked about, so you need one call per license.
Listing Views¶
GET /v2/permissions/views
| Parameter | Required | Description |
|---|---|---|
licenseNumber | yes | The license to check |
import requests
views = requests.get(
"https://api.trackandtrace.tools/v2/permissions/views",
params={"licenseNumber": "LIC-00001"},
headers={"Authorization": f"Bearer {access_token}"},
).json()
print(views)
[
"admin/items",
"admin/strains",
"admin/tags",
"packages",
"plants",
"transfers/hub",
"transfers/licensed"
]
The list is sorted and deduplicated. A view missing from this list means the account cannot open that page. That is the check to make before assuming a collection endpoint will work.
Listing Permissions Within a View¶
GET /v2/permissions
| Parameter | Required | Description |
|---|---|---|
licenseNumber | yes | The license to check |
view | yes | A view name, as returned by /v2/permissions/views |
import requests
permissions = requests.get(
"https://api.trackandtrace.tools/v2/permissions",
params={"licenseNumber": "LIC-00001", "view": "packages"},
headers={"Authorization": f"Bearer {access_token}"},
).json()
print(permissions)
[
"packages:new_packages",
"packages:remediate",
"packages:adjust_packages",
"packages:change_notes"
]
view must be a view name — one or more slash-separated alphanumeric segments, such as packages or transfers/licensed. Anything else is rejected with a 400 and the code INVALID_QUERY_PARAMETER.
The view:action Format¶
Every entry is the view you asked for, a colon, then the action:
Because a view can contain slashes, split on the first colon, not on the slash:
Action names are derived from the labels Metrc renders on the page, lowercased with spaces replaced by underscores — the New Packages button becomes packages:new_packages. They are descriptive, not a fixed vocabulary: the available actions differ by state and license class, which is the same reason the view list does.
Checking Before You Act¶
The two endpoints compose into a single check. Ask for the view first, then the action only if you need that level of detail:
import requests
BASE = "https://api.trackandtrace.tools"
HEADERS = {"Authorization": f"Bearer {access_token}"}
def can(*, license_number: str, permission: str) -> bool:
"""True if the account may perform `permission` on `license_number`.
Accepts either a view ("packages") or a view:action pair
("packages:remediate").
"""
view, _, action = permission.partition(":")
views = requests.get(
f"{BASE}/v2/permissions/views",
params={"licenseNumber": license_number},
headers=HEADERS,
).json()
if view not in views:
return False
if not action:
return True
permissions = requests.get(
f"{BASE}/v2/permissions",
params={"licenseNumber": license_number, "view": view},
headers=HEADERS,
).json()
return permission in permissions
print(can(license_number="LIC-00001", permission="packages"))
print(can(license_number="LIC-00001", permission="packages:remediate"))
print(can(license_number="LIC-00001", permission="transfers/licensed:new_transfer"))
If you are checking several actions on the same page, fetch the view's permissions once and test against the list rather than calling can() repeatedly.
Limits and Behavior¶
- One license per request. Both endpoints take exactly one
licenseNumber. Checking a whole account means one views call per license. - Detail costs one call per view.
/v2/permissionscovers a single view, so enumerating everything is one request per view. Fetch only the views you care about. - Results are live. Each call reflects your permissions at that moment. A grant changed inside Metrc shows up on the next request, with no cache to wait out.
- Free. No T3+ subscription required.
Examples¶
Substitute your API token and license number into each example.
List every view available on a license:
curl 'https://api.trackandtrace.tools/v2/permissions/views?licenseNumber=LIC-00001' \
-H 'Authorization: Bearer <TOKEN>'
List the actions available on the packages page:
curl 'https://api.trackandtrace.tools/v2/permissions?licenseNumber=LIC-00001&view=packages' \
-H 'Authorization: Bearer <TOKEN>'
A view containing a slash — URL-encode it, or quote the URL:
curl 'https://api.trackandtrace.tools/v2/permissions?licenseNumber=LIC-00001&view=transfers/licensed' \
-H 'Authorization: Bearer <TOKEN>'
Find every license that can create packages:
import requests
BASE = "https://api.trackandtrace.tools"
HEADERS = {"Authorization": f"Bearer {access_token}"}
licenses = requests.get(f"{BASE}/v2/licenses", headers=HEADERS).json()
for license in licenses:
number = license["licenseNumber"]
permissions = requests.get(
f"{BASE}/v2/permissions",
params={"licenseNumber": number, "view": "packages"},
headers=HEADERS,
).json()
if "packages:new_packages" in permissions:
print(number)
Next Steps¶
- Getting Started — authenticate and make your first request
- Search — query across collections without knowing which one to look in
- Supercollections — load a collection and its metadata in one request