Creating Plant Batches¶
Three endpoints cover the path from a package of seeds or clones to tracked plants in the Flowering state:
| Step | Endpoint | Result |
|---|---|---|
| 1 | POST /v2/plantbatches/create/from-packages | A package becomes an immature plant batch |
| 2 | POST /v2/plantbatches/promote | Immature plants become tagged Vegetative or Flowering plants |
| 3 | POST /v2/plants/changegrowthphase | Vegetative plants become Flowering |
You do not always need all three. Step 2 can promote straight to Flowering in states that allow it, in which case step 3 is unnecessary.
Like every mutation in this API, these are dry runs by default. Add submit=true to actually write to Metrc — see Getting Started.
Step 0 — Fetch the input options¶
GET /v2/plantbatches/create/from-packages/inputs returns every option list needed to build the create payload:
import requests
API = "https://api.trackandtrace.tools"
HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
inputs = requests.get(
f"{API}/v2/plantbatches/create/from-packages/inputs",
params={"licenseNumber": "CUL00001"},
headers=HEADERS,
).json()
print([t["name"] for t in inputs["plantBatchTypes"]]) # ['Clone', 'Seed']
print([s["name"] for s in inputs["strains"]])
This is the only source for plantBatchTypeId. The value is state-specific and does not appear on the plant batch records returned by GET /v2/plantbatches/active, so there is no way to infer it from existing data.
Filter locations on forPlantBatches — not every location can hold one:
tags is populated in California and null everywhere else, which mirrors the naming split described below.
Step 1 — Create a plant batch from a package¶
Draws material from a package and creates an immature plant batch.
import requests
API = "https://api.trackandtrace.tools"
HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
response = requests.post(
f"{API}/v2/plantbatches/create/from-packages",
params={"licenseNumber": "CUL00001", "submit": "true"},
headers=HEADERS,
json=[
{
"actualDate": "2026-08-26",
"plantedDate": "2026-08-26",
"locationId": 12345,
"packageId": 5077333,
"plantBatchTypeId": 1,
"plantsCount": 25,
"quantity": 25,
"strainId": 737,
"unitOfMeasureId": 4,
"tagId": 1527333,
}
],
)
response.raise_for_status()
quantity is how much of the source package is consumed; plantsCount is how many plants the new batch contains. They are frequently equal — 25 seeds drawn from a package measured in Each — but they are separate fields and need not match when the package is weight-based.
Naming differs by state¶
This is the one place where the payload changes shape depending on where you operate.
A plant batch is identified by a tag, and the batch name is that tag's label. Send tagId and omit name.
Find eligible tags with GET /v2/tags/available.
Sending both, or neither, is not valid. Which one applies is determined by the state of the license you are writing to, not by a setting on your account.
Step 2 — Promote immature plants¶
Moves plants out of the batch into a tracked growth phase, assigning one plant tag per plant.
response = requests.post(
f"{API}/v2/plantbatches/promote",
params={"licenseNumber": "CUL00001", "submit": "true"},
headers=HEADERS,
json=[
{
"growthDate": "2026-09-15",
"growthPhase": "Vegetative",
"id": 987654,
"newLocationId": 12345,
"plantsCount": 25,
"startingTagId": 1527333,
"endingTagId": 1527357,
}
],
)
response.raise_for_status()
startingTagId and endingTagId bound a contiguous range of tags. Every tag from start to end inclusive is consumed, so plantsCount must equal the size of that range — 25 plants requires exactly 25 consecutive available tags.
That constraint is the most common source of errors here. Tags returned by GET /v2/tags/available are not guaranteed to be contiguous, so pick a run of adjacent ids rather than the first 25 in the list:
tags = requests.get(
f"{API}/v2/tags/available",
params={"licenseNumber": "CUL00001", "pageSize": 500},
headers=HEADERS,
).json()["data"]
ids = sorted(tag["id"] for tag in tags)
# Find the first run of `needed` consecutive ids
needed = 25
run_start = 0
for index in range(1, len(ids)):
if ids[index] != ids[index - 1] + 1:
run_start = index
elif index - run_start + 1 == needed:
starting_tag_id, ending_tag_id = ids[run_start], ids[index]
break
else:
raise RuntimeError(f"No run of {needed} consecutive tags is available")
Step 3 — Change growth phase¶
Moves plants between phases, most commonly Vegetative to Flowering.
response = requests.post(
f"{API}/v2/plants/changegrowthphase",
params={"licenseNumber": "CUL00001", "submit": "true"},
headers=HEADERS,
json=[
{
"growthDate": "2026-10-01",
"growthPhase": "Flowering",
"id": 987654,
"newLocationId": None,
"newTagId": None,
}
],
)
response.raise_for_status()
newLocationId and newTagId must be present but may be null. Metrc expects to see both keys whether or not you are moving or retagging the plant. Omitting them is not the same as sending null.
Pass a real newLocationId to move the plant into a flowering room in the same request, and a real newTagId to retag it at the same time.
Finding the ids¶
Every numeric id above comes from a list endpoint:
| Field | Where it comes from |
|---|---|
plantBatchTypeId | GET /v2/plantbatches/create/from-packages/inputs → plantBatchTypes |
strainId | GET /v2/plantbatches/create/from-packages/inputs → strains |
locationId, newLocationId | GET /v2/plantbatches/create/from-packages/inputs → locations |
unitOfMeasureId | GET /v2/plantbatches/create/from-packages/inputs → unitsOfMeasure |
packageId | GET /v2/packages/active |
tagId, startingTagId, endingTagId, newTagId | GET /v2/tags/available |
id (plant batch) | GET /v2/plantbatches/active |
id (plant) | GET /v2/plants/vegetative |
Verifying the result¶
Each step is visible in the corresponding list endpoint immediately:
batches = requests.get(
f"{API}/v2/plantbatches/active",
params={"licenseNumber": "CUL00001", "filter": "name__eq:Spring Batch A"},
headers=HEADERS,
).json()["data"]
assert len(batches) == 1
print(batches[0]["untrackedCount"])
After promoting, the promoted plants appear in GET /v2/plants/vegetative (or /v2/plants/flowering) and the batch's untracked count drops by plantsCount.