# PlantLab Agent Reference

## Base URL

`https://api.plantlab.ai`

## Authentication

- Header: `X-API-Key`
- Public endpoints requiring auth: `/info`, `/diagnose`, `/feedback`, `/usage`, `/history`
- Health endpoint: `/health`

## Diagnose

`POST /diagnose` — multipart/form-data upload. Authenticated with `X-API-Key`.

### Request fields

| Field | Type | Required | Notes |
|---|---|---|---|
| `image` | binary | yes | JPEG or PNG, max 32 MB. A photo above 8192 px on a side or 50 million pixels is downscaled to fit. |
| `installation_id` | string | no | Optional device identifier. Availability and limits depend on account access. |

### Response handling

Every successful diagnose response has `schema_version` `4.2.0`. The top level includes:
- `request_id`
- `schema_version`
- `engine_version` — object `{api, models}` identifying the API build and the global model iteration that served this diagnosis. Use it to detect engine upgrades and react (e.g., invalidate caches, recompute thresholds).
- `success`
- `species` — a species ID, or `null`. Known values are `cannabis` and `tomato`. The API sets this field before plant diagnosis.
- `species_name` — scientific name of the species, or `null` when `species` is `null`. The field is always present.
- `species_confidence` — confidence in the detected species, or `null` if `species` is `null`.
- `in_scope` — `true` for cannabis and tomato. It is `false` for an unknown species.
- `routed_reason` — `unknown_species` if the API identifies neither species. It is `null` for cannabis and tomato.
- `results` — one entry per diagnosed plant. Cannabis diagnoses appear here. A tomato response has one entry with a full-frame `bbox`, a health verdict, and, for an unhealthy plant, one named cause. An unknown species returns an empty array.
- `stage_times` — image-level

Each `results[]` entry includes:
- `bbox` — `{x0, y0, x1, y1, normalized}` in normalized `[0,1]` image coordinates. A whole-image / single-plant result is `{0,0,1,1}`. Multiply by image width/height to draw the box.

Depending on the inference path each entry may also include:
- `is_healthy`
- `health_confidence`
- `growth_stage`
- `growth_stage_confidence`
- `conditions` — array of predictions `{class_id, confidence, suspected?, coarse_group?}`. `coarse_group` is the clinical group of the fine class (`mobile_nutrient`, `immobile_newgrowth`, `water`, `light`, `fungal_disease`, `pest`); use it to hedge when a confusable fine class is uncertain rather than asserting the exact class.
- `pests` — array of the same prediction shape (`coarse_group` is `pest`).
- `mulders_hypotheses`
- `coarse_fallback` — string, one of the six coarse groups, present ONLY when the plant's top-1 condition/pest confidence is below the server's threshold. When present, the fine class is uncertain and the coarse group is the more reliable read; prefer surfacing the group over asserting the exact class. Absent when the top-1 is confident.
- `reliability_score`
- `uncertainty_factors`
- `environmental_patterns`
- `progression_risks`

Species fields replace the top-level cannabis fields. A tomato result identifies the plant and gives a health verdict. An unhealthy tomato result names one cause in `conditions` or `pests`. Spider mites and leaf miners are pests. It has no growth stage. `species` can be any ID that matches `^[a-z][a-z0-9_]*$`; known values are `cannabis` and `tomato`.

## Usage

`GET /usage` returns the current daily and monthly usage for the authenticated API key without consuming quota.

Example response:

```json
{
  "success": true,
  "tier": "free",
  "usage": {
    "daily": {
      "used": 2,
      "limit": 3,
      "remaining": 1,
      "period_start": "2026-04-20",
      "period_end": "2026-04-20",
      "resets_at": "2026-04-21T00:00:00Z"
    },
    "monthly": {
      "used": 12,
      "limit": 90,
      "remaining": 78,
      "period_start": "2026-04-01",
      "period_end": "2026-04-30",
      "resets_at": "2026-05-01T00:00:00Z"
    }
  }
}
```

`remaining` is clamped to `0` and never goes negative. On database error the endpoint returns `503` with code `USAGE_CHECK_UNAVAILABLE`.

## History

`GET /history` — retrieve paginated diagnosis history for the authenticated user.

Diagnosis history stores results only when enabled for the account. It does not include photos. The service is currently free. Read account access and limits from the API.

### Query parameters

| Parameter | Type | Default | Notes |
|---|---|---|---|
| `cursor` | string | — | Opaque pagination cursor. Pass back the previous response's `next_cursor` to fetch the next page. When present, `since` is ignored. |
| `since` | string (RFC3339) | available history window start | Only records created after this timestamp are returned. If the value is earlier than the available history window start it is silently ignored and the full window is used. Ignored when `cursor` is present. |
| `limit` | integer | 100 | Maximum records to return. Clamped to the tier maximum. |
| `installation_id` | string | — | Filter to records tagged with this identifier. If omitted, all records within the available window are returned. |

### Pagination

`GET /history` uses cursor-based pagination. The response includes `next_cursor` when more data is available; the field is absent on the last page.

Pass `next_cursor` back as `?cursor=` to fetch the next page:

```bash
# Page 1 — no cursor
curl https://api.plantlab.ai/history \
  -H "X-API-Key: pl_live_your_api_key" \
  -G --data-urlencode "limit=50"
# → {"items":[...], "count":50, "next_cursor":"<opaque>"}

# Page 2 — pass next_cursor
curl https://api.plantlab.ai/history \
  -H "X-API-Key: pl_live_your_api_key" \
  -G --data-urlencode "limit=50" \
     --data-urlencode "cursor=<opaque>"
# → {"items":[...], "count":12}  (no next_cursor → last page)
```

The cursor value is opaque. Do not parse or construct it; only pass it back verbatim.

### Sample request

```bash
curl https://api.plantlab.ai/history \
  -H "X-API-Key: pl_live_your_api_key" \
  -G \
  --data-urlencode "since=2026-05-01T00:00:00Z" \
  --data-urlencode "limit=50" \
  --data-urlencode "installation_id=ha-instance-001"
```

### Sample 200 response

```json
{
  "items": [
    {
      "id": 1042,
      "request_id": "550e8400-e29b-41d4-a716-446655440000",
      "installation_id": "ha-instance-001",
      "class_id": "nitrogen_deficiency",
      "confidence": 0.85,
      "species": "cannabis",
      "species_confidence": 0.95,
      "in_scope": true,
      "routed_reason": null,
      "is_healthy": false,
      "growth_stage": "flowering",
      "growth_stage_confidence": 0.9,
      "conditions": [
        { "class_id": "nitrogen_deficiency", "confidence": 0.85, "coarse_group": "mobile_nutrient" }
      ],
      "pests": [],
      "nutrient_analysis": null,
      "engine_version": "1.0.179+v1",
      "created_at": "2026-05-08T12:00:00Z"
    }
  ],
  "count": 1,
  "next_cursor": "eyJ0IjoiMjAyNi0wNS0wOFQxMjowMDowMFoiLCJpZCI6MTA0Mn0"
}
```

### Error responses

| Status | Code | Condition |
|---|---|---|
| 400 | — | `cursor` value is malformed or unrecognizable |
| 401 | `UNAUTHORIZED` | Missing or invalid API key |
| 403 | `HISTORY_NOT_AVAILABLE` | History is unavailable for this account |
| 403 | `HISTORY_OPT_IN_REQUIRED` | Diagnosis history is disabled |
| 500 | `HISTORY_FETCH_FAILED` | Database error retrieving records |

## Error handling

Diagnose errors are returned as JSON with:
- `request_id`
- `success`
- `error`
- `code`

Typical codes include:
- `FILE_UPLOAD_ERROR`
- `FILE_TOO_LARGE`
- `INVALID_FORMAT`
- `INFERENCE_TIMEOUT`
- `UNAUTHORIZED`
- `DAILY_LIMIT_EXCEEDED`
- `MONTHLY_LIMIT_EXCEEDED`
- `HISTORY_NOT_AVAILABLE` (history access unavailable)
- `HISTORY_OPT_IN_REQUIRED` (diagnosis history disabled)
- `REQUEST_NOT_FOUND` (`/feedback` for a `request_id` not owned by the caller)
- `FORBIDDEN` (operational endpoints `/health/detailed` and `/metrics` accessed with an end-user API key — these are reserved for service-auth callers)

## Rate limiting

When a request is throttled, PlantLab returns:
- HTTP `429`
- `Retry-After` header
- structured JSON describing the limit condition

For usage-limit throttling, the JSON body includes:
- `retryAfter`
- `limitType`
- `limit`
- `used`

Free tier allows 3 diagnoses per day. The `limit` and `used` fields in the 429 body reflect the caller's current tier allocation.

## Reliability score

`reliability_score` is an optional estimate of result reliability for unhealthy cannabis. It differs from per-condition confidence. Use it to route results for closer review. It does not guarantee correctness.

Use it as a routing signal or warn the user when the score is low. It is independent from per-class `confidence` (the score for a single class).

## Compatibility guidance

- Preserve unknown response fields instead of dropping them
- Use `request_id` as the stable correlation key for downstream flows
- Treat additive response fields as non-breaking

## Species capabilities

Cannabis (`Cannabis sativa`) and tomato (`Solanum lycopersicum`) have live diagnosis. Tomato diagnosis is available. For an unrecognized species, the API returns `species: null`, `in_scope: false`, and `routed_reason: "unknown_species"`. Mixed-species diagnosis in one photo is not supported.

Tomato classes are `early_blight`, `late_blight`, `leaf_mold`, `septoria`, `bacterial_spot`, `leaf_curl_virus`, `mosaic_virus`, `target_spot`, `spotted_wilt_virus`, `magnesium_deficiency`, `nitrogen_deficiency`, `potassium_deficiency`, and the pests `spider_mites` and `leaf_miners`. Healthy is a separate state. An optional `suspected: true` describes a suspected condition, not a healthy result.
