{
  "openapi": "3.1.0",
  "info": {
    "title": "PlantLab Public API",
    "version": "3.1.0",
    "summary": "Public HTTP API for plant diagnosis, verification polling, and feedback submission.",
    "description": "Canonical machine-readable contract for external PlantLab integrations. Third-party agents should integrate directly with https://api.plantlab.ai using X-API-Key."
  },
  "servers": [
    {
      "url": "https://api.plantlab.ai",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "summary": "Health check",
        "security": [],
        "responses": {
          "200": {
            "description": "Service health",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string" },
                    "models_loaded": { "type": "integer" },
                    "version": { "type": "string" }
                  },
                  "required": ["status"]
                }
              }
            }
          }
        }
      }
    },
    "/info": {
      "get": {
        "summary": "API metadata and capabilities",
        "responses": {
          "200": {
            "description": "API info",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/InfoResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/diagnose": {
      "post": {
        "summary": "Diagnose plant health from an uploaded image",
        "parameters": [
          {
            "name": "X-Verify",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["true"]
            },
            "description": "Request asynchronous verification when the subscription tier allows it."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  },
                  "installation_id": {
                    "type": "string",
                    "maxLength": 256,
                    "description": "Optional identifier for the installation or device submitting this request. Requires Pro tier or higher. Ignored on the free tier (returns 400 if provided). Each tier enforces a per-account cap on distinct installation IDs (Pro: 100, Business: 1000)."
                  }
                },
                "required": ["image"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Diagnosis result",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DiagnoseResponse" },
                "examples": {
                  "unhealthy_cannabis": {
                    "value": {
                      "request_id": "550e8400-e29b-41d4-a716-446655440000",
                      "schema_version": "3.1.0",
                      "engine_version": { "api": "1.0.158", "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.85,
                          "growth_stage": "flowering",
                          "growth_stage_confidence": 0.9,
                          "conditions": [
                            { "class_id": "nitrogen_deficiency", "confidence": 0.85, "coarse_group": "mobile_nutrient" }
                          ],
                          "reliability_score": 0.87
                        }
                      ],
                      "verification": {
                        "status": "pending",
                        "verification_id": "123e4567-e89b-12d3-a456-426614174000"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid upload or image",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "408": {
            "description": "Inference timeout",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/UsageRateLimitErrorResponse" },
                    { "$ref": "#/components/schemas/ErrorResponse" }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/usage": {
      "get": {
        "summary": "Get current API usage for the authenticated user",
        "description": "Returns daily and monthly diagnosis usage for the API key owner, including counts used, configured limits, remaining quota, and period boundaries. This endpoint is read-only and does not consume quota.",
        "responses": {
          "200": {
            "description": "Daily and monthly usage with limits and period info"
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "503": {
            "description": "Usage service unavailable"
          }
        }
      }
    },
    "/verification/{id}": {
      "get": {
        "summary": "Poll verification status",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Verification status",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/VerificationResponse" }
              }
            }
          },
          "400": {
            "description": "Invalid verification id",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Tier restricted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Verification not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/history": {
      "get": {
        "summary": "Retrieve paginated diagnosis history",
        "description": "Returns stored diagnosis results for the authenticated user, ordered newest-first. Only available to paid-tier users (Pro or higher) who have enabled the data-sharing preference (OptInTraining). Free-tier callers receive 403. The same opt-in preference governs both training data collection and diagnosis history retention. Pro: up to 1000 records within a 90-day window. Business: up to 5000 records within a 365-day window.",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor. Pass back the previous response's next_cursor value to fetch the next page. When cursor is present, the since parameter is ignored."
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "RFC3339 timestamp. Only records created after this time are returned. If omitted or earlier than the tier window start, the full retention window is used. Ignored when cursor is also present."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 100
            },
            "description": "Maximum number of records to return. Clamped to the tier maximum (Pro: 1000, Business: 5000). Defaults to 100."
          },
          {
            "name": "installation_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter results to records tagged with this installation identifier. If omitted, all records within the window are returned."
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated history records",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HistoryResponse" },
                "examples": {
                  "history_page": {
                    "value": {
                      "items": [
                        {
                          "id": 1042,
                          "request_id": "550e8400-e29b-41d4-a716-446655440000",
                          "installation_id": "ha-instance-001",
                          "class_id": "nitrogen_deficiency",
                          "confidence": 0.85,
                          "is_cannabis": true,
                          "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.158+v6",
                          "created_at": "2026-05-08T12:00:00Z"
                        }
                      ],
                      "count": 1,
                      "next_cursor": "eyJ0IjoiMjAyNi0wNS0wOFQxMjowMDowMFoiLCJpZCI6MTA0Mn0"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid cursor value",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "History not available. HISTORY_NOT_AVAILABLE = free tier (no history retention). HISTORY_OPT_IN_REQUIRED = paid tier with opt_in_training=false.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/feedback": {
      "post": {
        "summary": "Submit feedback for a prior diagnose response",
        "description": "Submits accuracy feedback for a previously issued request_id. The request_id MUST belong to the authenticated caller; feedback for another user's request_id is rejected with 404 REQUEST_NOT_FOUND. The same response is returned whether the request_id was never issued, was issued to a different user, or has aged out of the 30-day retention window — clients cannot use this endpoint to enumerate request IDs.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/FeedbackRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Feedback stored",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/FeedbackResponse" }
              }
            }
          },
          "400": {
            "description": "Invalid request body, missing or malformed request_id, or oversized diagnosis_result",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "request_id not found for the authenticated caller (REQUEST_NOT_FOUND). Same response when the id was never issued, was issued to a different user, or aged out.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      }
    },
    "headers": {
      "XRequestId": {
        "description": "Request correlation identifier. Matches request_id in diagnose responses.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "RetryAfter": {
        "description": "Number of seconds the client should wait before retrying.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "request_id": { "type": "string" },
          "success": { "type": "boolean" },
          "error": { "type": "string" },
          "code": { "type": "string" }
        },
        "required": ["success", "error"]
      },
      "HistoryItem": {
        "type": "object",
        "required": ["id", "request_id", "class_id", "confidence", "is_cannabis", "is_healthy", "conditions", "pests", "engine_version", "created_at"],
        "properties": {
          "id": { "type": "integer", "format": "int64" },
          "request_id": { "type": "string" },
          "installation_id": { "type": "string" },
          "class_id": { "type": "string" },
          "confidence": { "type": "number", "format": "double" },
          "is_cannabis": { "type": "boolean" },
          "is_healthy": { "type": "boolean" },
          "growth_stage": { "type": "string", "nullable": true },
          "growth_stage_confidence": { "type": "number", "format": "double", "nullable": true },
          "conditions": {
            "type": "array",
            "items": { "type": "object", "additionalProperties": true }
          },
          "pests": {
            "type": "array",
            "items": { "type": "object", "additionalProperties": true }
          },
          "nutrient_analysis": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "engine_version": { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "HistoryResponse": {
        "type": "object",
        "required": ["items", "count"],
        "properties": {
          "items": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/HistoryItem" }
          },
          "count": { "type": "integer" },
          "next_cursor": {
            "type": "string",
            "description": "Opaque cursor; absent on the last page. Pass as ?cursor= to fetch the next page."
          }
        }
      },
      "UsageRateLimitErrorResponse": {
        "allOf": [
          { "$ref": "#/components/schemas/ErrorResponse" },
          {
            "type": "object",
            "properties": {
              "retryAfter": { "type": "integer" },
              "limitType": { "type": "string", "enum": ["daily", "monthly"] },
              "limit": { "type": "integer" },
              "used": { "type": "integer" }
            },
            "required": ["retryAfter", "limitType", "limit", "used"]
          }
        ]
      },
      "ClassPrediction": {
        "type": "object",
        "properties": {
          "class_id": { "type": "string" },
          "confidence": { "type": "number" },
          "suspected": { "type": "boolean" },
          "coarse_group": {
            "type": "string",
            "description": "Clinical coarse group of class_id (mobile_nutrient, immobile_newgrowth, water, light, fungal_disease, pest). Additive hedging signal for confusable fine classes; omitted for an unmapped class.",
            "enum": ["mobile_nutrient", "immobile_newgrowth", "water", "light", "fungal_disease", "pest"]
          }
        },
        "required": ["class_id", "confidence"]
      },
      "MuldersHypothesis": {
        "type": "object",
        "properties": {
          "excess": { "type": "string" },
          "explains": {
            "type": "array",
            "items": { "type": "string" }
          },
          "evidence": { "type": "number" },
          "evidence_count": { "type": "integer" }
        }
      },
      "VerificationInfo": {
        "type": "object",
        "properties": {
          "status": { "type": "string" },
          "verification_id": { "type": "string", "format": "uuid" },
          "agreement": { "type": "string" },
          "second_opinion": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": ["status"]
      },
      "DiagnoseResponse": {
        "type": "object",
        "properties": {
          "request_id": { "type": "string", "format": "uuid" },
          "schema_version": { "type": "string" },
          "engine_version": {
            "type": "object",
            "description": "API build and global model iteration that served this diagnosis. Use to detect engine upgrades and react (cache invalidation, threshold recompute). Optional; absent on older builds.",
            "properties": {
              "api": {
                "type": "string",
                "description": "Deployed API build version (matches GET /info)."
              },
              "models": {
                "type": "string",
                "description": "Lowest model iteration label across the diagnostic stages (e.g. 'v3'). Bumps when any stage retrains."
              }
            },
            "required": ["api", "models"]
          },
          "success": { "type": "boolean" },
          "is_cannabis": {
            "type": "boolean",
            "description": "Whether the image was identified as cannabis. Image-level (decided before the per-plant split), so it stays at the top level."
          },
          "cannabis_confidence": {
            "type": "number",
            "description": "Confidence in the image-level cannabis decision. Top-level for the same reason as is_cannabis."
          },
          "results": {
            "type": "array",
            "description": "One diagnosis per detected plant. A single-plant image returns a length-1 array. Iterate this array; the per-plant diagnosis fields live on each entry, not at the top level (schema 3.0.0 breaking change from 2.x).",
            "items": { "$ref": "#/components/schemas/PlantDiagnosis" }
          },
          "stage_times": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            }
          },
          "verification": { "$ref": "#/components/schemas/VerificationInfo" }
        },
        "required": [
          "request_id",
          "schema_version",
          "success",
          "is_cannabis",
          "cannabis_confidence",
          "results"
        ]
      },
      "BBoxNormalized": {
        "type": "object",
        "description": "The region of the image occupied by this plant, in normalized [0,1] coordinates (x0,y0 = top-left; x1,y1 = bottom-right). A whole-image / single-plant result is {0,0,1,1}. Multiply by the image width/height to draw the box.",
        "properties": {
          "x0": { "type": "number", "minimum": 0, "maximum": 1 },
          "y0": { "type": "number", "minimum": 0, "maximum": 1 },
          "x1": { "type": "number", "minimum": 0, "maximum": 1 },
          "y1": { "type": "number", "minimum": 0, "maximum": 1 },
          "normalized": {
            "type": "boolean",
            "description": "Always true; documents that the coordinates are normalized to [0,1]."
          }
        },
        "required": ["x0", "y0", "x1", "y1", "normalized"]
      },
      "PlantDiagnosis": {
        "type": "object",
        "description": "Diagnosis for one detected plant. Health and growth fields appear once the plant is identified as cannabis; conditions, pests, reasoning layers and reliability_score appear only for an unhealthy plant.",
        "properties": {
          "bbox": { "$ref": "#/components/schemas/BBoxNormalized" },
          "is_healthy": { "type": "boolean" },
          "health_confidence": { "type": "number" },
          "growth_stage": { "type": "string" },
          "growth_stage_confidence": { "type": "number" },
          "conditions": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ClassPrediction" }
          },
          "pests": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ClassPrediction" }
          },
          "mulders_hypotheses": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MuldersHypothesis" }
          },
          "reliability_score": {
            "type": "number",
            "format": "float",
            "minimum": 0,
            "maximum": 1,
            "description": "Learned probability that this plant's top condition/pest prediction is correct (1.0 = highly reliable, 0.0 = very likely wrong). Present only when the staged pipeline reached Stage 2 (cannabis-detected and unhealthy). Use for triaging which diagnoses to escalate to verification or surface to the user as 'low confidence'. Independent from per-class `confidence` (which is the model's raw probability for one specific class)."
          },
          "coarse_fallback": {
            "type": "string",
            "enum": ["mobile_nutrient", "immobile_newgrowth", "water", "light", "fungal_disease", "pest"],
            "description": "Abstention hedge: the clinical coarse group of this plant's top-1 condition/pest, emitted ONLY when that top-1 confidence is below the server's coarse-fallback threshold. Present means the fine class is uncertain and the coarse group is the more reliable read. Absent when the top-1 is confident. Prefer surfacing this group over asserting the exact fine class when it is present."
          },
          "uncertainty_factors": {
            "type": "array",
            "items": { "type": "string" }
          },
          "environmental_patterns": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "progression_risks": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        },
        "required": ["bbox"]
      },
      "VerificationResponse": {
        "type": "object",
        "properties": {
          "status": { "type": "string" },
          "verification_id": { "type": "string", "format": "uuid" },
          "agreement": { "type": "string" },
          "second_opinion": {
            "type": "object",
            "additionalProperties": true
          },
          "ensemble": {
            "type": "object",
            "additionalProperties": true
          },
          "tiebreaker_consulted": { "type": "boolean" },
          "created_at": { "type": "string" },
          "completed_at": { "type": "string" },
          "error": { "type": "string" }
        },
        "required": ["status", "verification_id"]
      },
      "FeedbackRequest": {
        "type": "object",
        "properties": {
          "request_id": { "type": "string", "format": "uuid" },
          "is_accurate": { "type": "boolean" },
          "diagnosis_result": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": ["request_id", "is_accurate"]
      },
      "FeedbackResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "feedback_id": { "type": "string", "format": "uuid" }
        },
        "required": ["success", "feedback_id"]
      },
      "InfoResponse": {
        "type": "object",
        "properties": {
          "api": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "version": { "type": "string" },
              "description": { "type": "string" },
              "environment": { "type": "string" }
            },
            "required": ["name", "version", "description", "environment"]
          },
          "models": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "name": { "type": "string" },
                "description": { "type": "string" },
                "type": { "type": "string" }
              }
            }
          },
          "capabilities": {
            "type": "object",
            "properties": {
              "image_formats": {
                "type": "array",
                "items": { "type": "string" }
              },
              "max_file_size_mb": { "type": "integer" },
              "pipeline_stages": { "type": "integer" },
              "early_exit_enabled": { "type": "boolean" }
            },
            "required": ["image_formats", "max_file_size_mb", "pipeline_stages", "early_exit_enabled"]
          }
        },
        "required": ["api", "models", "capabilities"]
      }
    }
  }
}
