PlantLab Blog

The Cannabis Plant Diagnosis API: What One Photo Gets You Back

This article includes historical API examples. Read the current API documentation for supported plants and fields.

A herbarium specimen sheet in antique botanical style - a pressed cannabis leaf drawn in fine engraved ink on the left, joined by a single ruled line to a naturalist’s record card of neat abstract pen strokes on the right

What It Does

The PlantLab API takes one image of a cannabis plant and returns structured JSON describing what’s wrong with it: one of 30 conditions and pests (or healthy), a growth stage, a confidence score on every call it makes, and a bounding box per plant when the photo has more than one. Auth is a single X-API-Key header, and the free tier is 3 diagnoses a day without a card.

If you already have a camera pointed at your tent, this is the piece that turns a JPEG into something your automation can branch on.

Most grow stacks are blind in the same place. Temperature, humidity, VPD, EC, runoff pH, substrate moisture at three depths - all of it describes the room, none of it describes the plant. So the loop still ends at a person squinting at a phone.

Every response below came out of the live engine.


One Call

curl -X POST https://api.plantlab.ai/diagnose \
  -H "X-API-Key: $PLANTLAB_API_KEY" \
  -F "image=@canopy.jpg"

Multipart upload, one required field.


What Comes Back

A real response, from a plant with powdery mildew on it:

{
  "request_id": "8919a46e-a704-4a4e-a700-b754188165b5",
  "schema_version": "3.1.0",
  "engine_version": { "api": "1.0.166", "models": "v6" },
  "success": true,
  "is_cannabis": true,
  "cannabis_confidence": 0.95,
  "results": [
    {
      "bbox": { "x0": 0, "y0": 0, "x1": 1, "y1": 1, "normalized": true },
      "is_healthy": false,
      "health_confidence": 0.1,
      "growth_stage": "vegetative",
      "growth_stage_confidence": 0.9,
      "conditions": [
        {
          "class_id": "powdery_mildew",
          "display_name": "Powdery Mildew",
          "confidence": 0.8,
          "coarse_group": "fungal_disease"
        },
        {
          "class_id": "potassium_deficiency",
          "display_name": "Potassium Deficiency",
          "confidence": 0.6,
          "suspected": true,
          "coarse_group": "mobile_nutrient"
        }
      ]
    }
  ]
}

It returned two things. The mildew is the call it’s making. The potassium deficiency is marked suspected - something else worth a look, without claiming it.


The Fields

FieldLevelWhat it means
is_cannabisimageWhether the photo is cannabis at all. Decided first, so it sits at the top
cannabis_confidenceimageProbability the image is cannabis
results[]imageOne entry per detected plant. A single-plant photo returns an array of one
bboxplantWhere this plant is, in normalized 0-1 coordinates. Multiply by width and height to draw it
is_healthyplantThe health call for this plant
health_confidenceplantProbability the plant is healthy. Read the next section before using it
growth_stageplantseedling, vegetative, or flowering
conditions[]plantDiseases and deficiencies, most confident first
pests[]plantPests, same shape
schema_versionresponseContract version, currently 3.1.0
engine_versionresponseThe build and model iteration that served this call

The field people get wrong

health_confidence is the probability the plant is healthy. It isn’t confidence in the verdict you were just handed.

In the response above, is_healthy is false and health_confidence is 0.1. That’s not a shaky answer - it’s a very confident sick one, because a 10% chance of healthy is a 90% chance of not. Write if health_confidence < 0.5: flag_uncertain() and you’ll fire uncertainty warnings on the clearest sick plants you own while staying quiet on the genuinely ambiguous ones.

When is_healthy is false, low health_confidence means more certain, not less.

suspected and coarse_group

suspected: true marks a secondary finding the engine wants to flag but isn’t asserting. Show it to a human; don’t dose on it.

coarse_group is the clinical family a condition belongs to - one of mobile_nutrient, immobile_newgrowth, water, light, fungal_disease, pest. Some problems genuinely look alike in a photograph, and the family is often right when the specific name is shaky. If you’re deciding whether to alert rather than what to dose, group on this instead.

Also in the response

Three more fields show up when they apply.

mulders_hypotheses names nutrient excesses that would explain the deficiency you’re looking at. A calcium excess locking out nitrogen looks exactly like a nitrogen shortage, and feeding more nitrogen makes it worse.

progression_risks says what this turns into if nothing changes.

stage_advisories adds context that depends on the plant’s stage. Lower-leaf yellowing in late flower is usually normal, and it says so rather than letting you chase it.


The Rest of the Surface

EndpointMethodPurpose
/diagnosePOSTThe one you came for
/usageGETCurrent counts, limits, remaining quota. Read-only, doesn’t consume quota
/healthGETLiveness. No key required
/infoGETMetadata and capabilities
/historyGETPast diagnoses, newest first. Pro tier and above, with data sharing enabled
/feedbackPOSTReport a wrong result

/usage is the one integrators forget exists and then rebuild badly. Poll it instead of counting your own calls.

The full machine-readable contract is at plantlab.ai/openapi.json, so generate a client rather than hand-rolling one.


Limits and Errors

Free tier is 3 diagnoses a day. No card, no trial clock, and nothing to cancel - paid plans aren’t live yet, so right now that’s the whole offer. Higher-volume tiers exist in the API for granted accounts, and paid access is coming.

429 means you hit a limit. 408 means inference timed out. 400 on upload usually means the image failed a sanity check rather than a malformed request.


What It Doesn’t Do

It’s cannabis-specific. Point it at a tomato and is_cannabis comes back false, which is correct and not useful.

It reads a photograph, so it’s bounded by what a photograph contains. Root-zone problems only appear once they reach the leaves, and two conditions that look identical in RGB are hard to separate in RGB. That’s why suspected and coarse_group exist instead of a single confident label.

It’s sensitive to how you shoot. Overexposure and tight close-ups reliably produce wrong answers - I measured what breaks a diagnosis, and the results aren’t what most people guess.

It doesn’t replace looking at your plants. It notices things earlier and more consistently than you will at 11pm, and it never gets bored.


Getting Started

Sign up at plantlab.ai, copy your key from the dashboard, and run the curl command at the top of this post against one of your own plants.

Prefer wires to code? There are guides for Home Assistant and Node-RED.