Managing Plants¶
Once a plant is tracked, these endpoints cover what you can do to it: move it, change its strain, take clones off it, retag it, package it, and destroy it.
| Endpoint | What it does |
|---|---|
POST /v2/plants/change-locations | Move plants to a different location |
POST /v2/plants/change-strains | Reassign plants to a different strain |
POST /v2/plants/changes-by-location/flowering | Page-level location and growth phase changes |
POST /v2/plants/changes-by-location/vegetative | The same, for vegetative plants |
POST /v2/plants/create/plantings | Take clones off a plant into a new plant batch |
POST /v2/plants/replace-tags | Move plants onto replacement tags |
POST /v2/plants/create/packages | Package vegetative plants |
POST /v2/plants/create/from-packages | Unpack a package of plants |
POST /v2/plants/assign-tags | Assign a run of tags to untagged plants |
POST /v2/plants/destroy | Destroy plants |
POST /v2/plants/merge | Merge one plant group into another |
POST /v2/plants/split | Split a plant group in two |
Like every mutation in this API, these are dry runs by default. Add submit=true to actually write to Metrc. See Writing to Metrc.
Not every endpoint exists in every state¶
These mirror Metrc's own interface, which differs by state.
| Endpoint | Available in |
|---|---|
merge, split | Maine only |
create/packages, assign-tags, create/from-packages | Everywhere except California |
changes-by-location/vegetative | Everywhere except California |
changegrowthphase | Everywhere except California |
California has no vegetative plant grid and no mother plant grid, which is why the vegetative-only operations are absent there.
Growth phase decides which inputs you get¶
Each write has one /inputs endpoint, whatever phase the plant is in. Changing growth phase is the exception, and it has three:
| Endpoint | Offers |
|---|---|
GET /v2/plants/changegrowthphase/inputs/vegetative | Vegetative, Flowering, Mother |
GET /v2/plants/changegrowthphase/inputs/flowering | Vegetative, Flowering |
GET /v2/plants/changegrowthphase/inputs/mother | Vegetative, Flowering |
Read the variant matching the phase your plant is in rather than assuming the lists agree. Although the vegetative list offers Mother, POST /v2/plants/changegrowthphase accepts only Vegetative and Flowering.
Promoting immature plants out of a batch has a single GET /v2/plantbatches/promote/inputs.
Every write has an inputs endpoint¶
import requests
API = "https://api.trackandtrace.tools"
HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
PARAMS = {"licenseNumber": "CUL00001"}
inputs = requests.get(
f"{API}/v2/plants/destroy/inputs", params=PARAMS, headers=HEADERS
).json()
for reason in inputs["actionReasons"]:
print(reason["id"], reason["name"], reason.get("requiresWasteWeight"))
This is the only reliable source for ids like wasteReasonId, which are state-specific and appear on no plant record.
Most writes have their own eligibility lookup¶
Metrc decides which plants are eligible for each operation, and the eligible set differs per operation. Use the source-plants endpoint belonging to the write you are about to make, not the general plant listing. change-locations and change-strains have none:
eligible = requests.get(
f"{API}/v2/plants/destroy/source-plants",
params={**PARAMS, "filter": "label__contains:1A40"},
headers=HEADERS,
).json()["data"]
Moving and changing strain¶
requests.post(
f"{API}/v2/plants/change-locations",
params={**PARAMS, "submit": "true"},
headers=HEADERS,
json=[{"id": 90210, "locationId": 12345, "actualDate": "2026-05-14"}],
)
sublocationId is accepted in Maine and Michigan only.
Changing strain takes no date:
requests.post(
f"{API}/v2/plants/change-strains",
params={**PARAMS, "submit": "true"},
headers=HEADERS,
json=[{"id": 90210, "strainId": 737}],
)
A harvested plant cannot change strain¶
A manicure takes product off a plant and leaves it flowering, so a plant that has already given a harvest sits in GET /v2/plants/flowering looking like any other. Metrc refuses a strain change on it:
There is no eligibility endpoint to ask. Read harvestCount off the plant instead, and only send plants where it is 0:
plants = requests.get(
f"{API}/v2/plants/flowering", params=PARAMS, headers=HEADERS
).json()["data"]
restrainable = [p for p in plants if p["harvestCount"] == 0]
Moving a harvested plant is still allowed. This restriction is specific to changing its strain.
Changes by location¶
This one is unusual: it is not tied to a selection of plants. Every change column is optional, so send only what changes.
requests.post(
f"{API}/v2/plants/changes-by-location/flowering",
params={**PARAMS, "submit": "true"},
headers=HEADERS,
json=[{
"id": 90210,
"changeDate": "2026-05-14",
"newLocationId": 12345,
"newGrowthPhase": "Flowering",
}],
)
Destroying¶
wasteWeight, wasteUnitOfMeasureId, plantWasteMethodId and materialMixed are required only for waste reasons that demand a weight.
A reason carries two flags, not one. requiresMatureWasteWeight governs tracked plants and requiresImmatureWasteWeight governs plants inside a plant batch. Read the one matching what you are destroying:
inputs = requests.get(
f"{API}/v2/plants/destroy/inputs", params=PARAMS, headers=HEADERS
).json()
for reason in inputs["actionReasons"]:
print(reason["name"], reason["requiresMatureWasteWeight"])
In some states every reason requires a weight for a tracked plant, so there may be no weight-free option. Omitting the weight is not rejected: Metrc answers 200 and does not perform the destruction, so always read the plant back.
requests.post(
f"{API}/v2/plants/destroy",
params={**PARAMS, "submit": "true"},
headers=HEADERS,
json=[{
"id": 90210,
"wasteReasonId": 3,
"reasonNote": "Failed phenotype",
"actualDate": "2026-05-14",
}],
)
countToDestroy is carried by Maine alone. Everywhere else the whole plant goes.
Packaging plants¶
Packages are cut from a list of plants, so the payload nests them under ingredients:
requests.post(
f"{API}/v2/plants/create/packages",
params={**PARAMS, "submit": "true"},
headers=HEADERS,
json=[{
"tagId": 442199,
"itemId": 7316223,
"locationId": 12345,
"ingredients": [{"plantId": 90210}, {"plantId": 90211}],
"actualDate": "2026-05-14",
}],
)
quantity inside an ingredient is carried by Maine alone.
isDonation, isTradeSample and isFromMotherPlant are checkboxes: send true to set one and omit it otherwise. Sending false is not the same as omitting it.
Unpacking plants¶
The reverse of packaging plants: vegetative plants come back out of a package made by POST /v2/plants/create/packages. A package of immature plants cut from a plant batch cannot be unpacked. Find the package with GET /v2/plants/create/from-packages/source-packages, and the type, strain and location ids on GET /v2/plants/create/from-packages/inputs:
inputs = requests.get(
f"{API}/v2/plants/create/from-packages/inputs", params=PARAMS, headers=HEADERS
).json()
requests.post(
f"{API}/v2/plants/create/from-packages",
params={**PARAMS, "submit": "true"},
headers=HEADERS,
json=[{
"packageId": 12345,
"plantBatchTypeId": inputs["plantBatchTypes"][0]["id"],
"plantsCount": 1,
"strainId": inputs["strains"][0]["id"],
"locationId": inputs["locations"][0]["id"],
"plantedDate": "2026-05-14",
"vegetativeDate": "2026-05-14",
"actualDate": "2026-05-14",
}],
)
startingTagId and endingTagId tag the plants from GET /v2/plants/create/from-packages/source-tags. Maine also requires tagTypeId, from inputs["tagTypes"], and Maine and Michigan accept a sublocationId.
Recording waste¶
Recording waste is not destruction. The plants stay in the grid; only the waste is recorded. There are three endpoints, differing in what the waste is attributed to:
| Endpoint | Attributed to |
|---|---|
POST /v2/plants/waste/record | named plants |
POST /v2/plantbatches/waste/record | a plant batch |
POST /v2/plants/waste/record-by-location | every plant in a location |
Every waste field is required here, unlike the destroy endpoints where the four waste fields are revealed only for reasons that demand a weight.
Note the note field: these endpoints call it wasteReasonNote, where destroy calls the same idea reasonNote.
requests.post(
f"{API}/v2/plants/waste/record",
params={**PARAMS, "submit": "true"},
headers=HEADERS,
json=[{
"plants": [{"id": 90210}],
"plantWasteMethodId": 1,
"materialMixed": "Soil",
"wasteWeight": 1.5,
"wasteUnitOfMeasureId": 4,
"wasteReasonId": 22,
"wasteReasonNote": "Routine pruning",
"actualDate": "2026-05-14",
}],
)
Reading waste back¶
waste = requests.get(
f"{API}/v2/plants/waste", params=PARAMS, headers=HEADERS
).json()["data"]
plants = requests.get(
f"{API}/v2/plants/waste/plants",
params={**PARAMS, "plantWasteId": waste[0]["id"]},
headers=HEADERS,
).json()["data"]
The sub-resources are addressed by plantWasteId, not id. The same shape applies to additives, addressed by plantAdditiveId:
| Endpoint | Returns |
|---|---|
GET /v2/plants/waste | every waste record |
GET /v2/plants/waste/plants | the plants in one record |
GET /v2/plants/waste/packages | packages produced by one record |
GET /v2/plants/additives | every additive application |
GET /v2/plants/additives/plants | the plants one application covered |
GET /v2/plants/additives/active-ingredients | its declared active ingredients |
Recording an additive application is not yet available through the API; the read endpoints above are.
Assigning a run of tags¶
startingTag and endingTag bound a contiguous run, and the plants in plants consume it in order. The run must be exactly as long as that list.
requests.post(
f"{API}/v2/plants/assign-tags",
params={**PARAMS, "submit": "true"},
headers=HEADERS,
json=[{
"startingTag": 442199,
"endingTag": 442201,
"plants": [{"id": 90210}, {"id": 90211}, {"id": 90212}],
"actualDate": "2026-05-14",
}],
)
Verifying the result¶
Read the state back rather than trusting the 200 (why). Unlike plant batches, a plant can be fetched by id:
plant = requests.get(
f"{API}/v2/plants/flowering/90210", params=PARAMS, headers=HEADERS
).json()
print(plant["locationName"], plant["strainName"])
That matters after a tag replacement in particular: the plant keeps its id, so reading by id is the only lookup the write does not move.
Next Steps¶
- Get plants into the system with Creating Plant Batches.
- Work with plant batches in Managing Plant Batches.
- Harvest what you grow with The Harvest Lifecycle.