{
  "openapi": "3.1.0",
  "info": {
    "title": "LeafAI API",
    "version": "1.0.0",
    "description": "Developer API for LeafAI leaf identification and user history.\n\nThis contract documents only the implemented backend endpoints and intentionally excludes non-existent auth endpoints such as `/auth/login` and `/auth/signup`."
  },
  "servers": [
    {
      "url": "http://localhost:8080",
      "description": "Local development"
    },
    {
      "url": "https://leafai-api-v2.adityamehrotra.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "System",
      "description": "Service health and operational status"
    },
    {
      "name": "Prediction",
      "description": "Leaf image analysis"
    },
    {
      "name": "History",
      "description": "User-specific prediction history"
    },
    {
      "name": "Assets",
      "description": "Uploaded image access"
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Health check",
        "description": "Returns backend health status.",
        "security": [],
        "responses": {
          "200": {
            "description": "Healthy service",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/predict": {
      "post": {
        "tags": [
          "Prediction"
        ],
        "summary": "Predict leaf species from image",
        "description": "Uploads a leaf image and returns enriched prediction metadata.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Leaf image file (`png`, `jpg`, `jpeg`, `gif`, `webp`, `bmp`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Prediction succeeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PredictSuccess"
                },
                "examples": {
                  "success": {
                    "value": {
                      "id": "c9f1f8d6-cc9f-4f4d-8d96-532f6ddab1f8",
                      "leafName": "Red Maple",
                      "scientificName": "Acer rubrum",
                      "origin": "Eastern North America",
                      "description": "Leaves are opposite, palmate, and have 3-5 pointed lobes with serrated margins.",
                      "accuracy": "high",
                      "accuracyPercent": 91.34,
                      "imageUrl": "http://localhost:8080/uploads/demoUid/c9f1f8d6-cc9f-4f4d-8d96-532f6ddab1f8.jpg",
                      "timestamp": "2026-03-05T17:03:12Z",
                      "confidence": 0.9134,
                      "care": {
                        "watering": "Water weekly, allowing topsoil to dry between watering.",
                        "sunlight": "Prefers full sun to partial shade.",
                        "soil": "Well-draining loamy soil with moderate organic matter."
                      },
                      "toxicity": "Non-toxic to humans",
                      "petSafety": "Safe for pets",
                      "seasonalInfo": "Shows bright red foliage in autumn before dropping leaves in late fall.",
                      "similarSpecies": [
                        {
                          "commonName": "Silver Maple",
                          "scientificName": "Acer saccharinum",
                          "keyDifference": "Silver maple has deeper sinuses and a silvery underside."
                        },
                        {
                          "commonName": "Sugar Maple",
                          "scientificName": "Acer saccharum",
                          "keyDifference": "Sugar maple lobes are smoother and less toothed."
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "missingField": {
                    "value": {
                      "message": "Missing upload field 'file'."
                    }
                  },
                  "noFileSelected": {
                    "value": {
                      "message": "No file selected."
                    }
                  },
                  "unsupportedType": {
                    "value": {
                      "message": "Unsupported file type. Upload an image file."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "missingHeader": {
                    "value": {
                      "message": "Missing or invalid Authorization header."
                    }
                  },
                  "missingToken": {
                    "value": {
                      "message": "Missing Firebase ID token."
                    }
                  },
                  "invalidToken": {
                    "value": {
                      "message": "Invalid or expired Firebase ID token."
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "Image exceeds MAX_UPLOAD_MB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "message": "This image is too large to upload. Please choose a file smaller than 10MB."
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Leaf cannot be reliably identified",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoLeafDetectedError"
                },
                "examples": {
                  "noLeaf": {
                    "value": {
                      "message": "Cannot detect a leaf in this image.",
                      "accuracyPercent": 0,
                      "confidence": 0
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "headers": {
              "Retry-After": {
                "description": "Number of seconds until the client may retry.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "rateLimited": {
                    "value": {
                      "message": "Rate limit reached for predictions. Limit is 10/minute, 25/hour, 50/day. Try again in 14s.",
                      "code": "rate_limit_exceeded",
                      "scope": "predict",
                      "window": "minute",
                      "retryAfterSeconds": 14
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server or model processing error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "predictionFailure": {
                    "value": {
                      "message": "Prediction failed. Please try again."
                    }
                  },
                  "malformedModelResponse": {
                    "value": {
                      "message": "Unexpected response from prediction model."
                    }
                  },
                  "missingRateLimitContext": {
                    "value": {
                      "message": "Missing authenticated user context for rate limiting."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/history": {
      "get": {
        "tags": [
          "History"
        ],
        "summary": "List prediction history",
        "description": "Returns authenticated user's predictions in descending timestamp order.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "History items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/HistoryItem"
                  }
                },
                "examples": {
                  "items": {
                    "value": [
                      {
                        "id": "c9f1f8d6-cc9f-4f4d-8d96-532f6ddab1f8",
                        "leafName": "Red Maple",
                        "scientificName": "Acer rubrum",
                        "origin": "Eastern North America",
                        "description": "Leaves are opposite, palmate, and have 3-5 pointed lobes with serrated margins.",
                        "accuracy": "high",
                        "imageUrl": "http://localhost:8080/uploads/demoUid/c9f1f8d6-cc9f-4f4d-8d96-532f6ddab1f8.jpg",
                        "timestamp": "2026-03-05T17:03:12Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          }
        }
      }
    },
    "/history/{history_id}": {
      "delete": {
        "tags": [
          "History"
        ],
        "summary": "Delete one history item",
        "description": "Deletes a history record owned by the authenticated user.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "history_id",
            "in": "path",
            "required": true,
            "description": "History record identifier.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deletion succeeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteHistoryResponse"
                },
                "examples": {
                  "deleted": {
                    "value": {
                      "message": "History item deleted."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "History item not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "message": "History item not found."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/uploads/{relative_path}": {
      "get": {
        "tags": [
          "Assets"
        ],
        "summary": "Fetch uploaded image",
        "description": "Returns an uploaded image file. Path must belong to the authenticated user's UID namespace.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "relative_path",
            "in": "path",
            "required": true,
            "description": "Relative storage path, for example: `uid/prediction-id.jpg`.",
            "allowReserved": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Image bytes",
            "content": {
              "image/jpeg": {},
              "image/png": {},
              "image/webp": {},
              "image/gif": {},
              "image/bmp": {}
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "File not found or unauthorized path scope"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Firebase ID Token",
        "description": "Set header as `Authorization: Bearer <firebase_id_token>`."
      }
    },
    "responses": {
      "UnauthorizedError": {
        "description": "Authentication failed",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "examples": {
              "missingHeader": {
                "value": {
                  "message": "Missing or invalid Authorization header."
                }
              },
              "invalidToken": {
                "value": {
                  "message": "Invalid or expired Firebase ID token."
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "HealthResponse": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          }
        }
      },
      "ApiError": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string"
          }
        }
      },
      "NoLeafDetectedError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiError"
          },
          {
            "type": "object",
            "required": [
              "accuracyPercent",
              "confidence"
            ],
            "properties": {
              "accuracyPercent": {
                "type": "number"
              },
              "confidence": {
                "type": "number"
              }
            }
          }
        ]
      },
      "RateLimitError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiError"
          },
          {
            "type": "object",
            "required": [
              "code",
              "scope",
              "window",
              "retryAfterSeconds"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "rate_limit_exceeded"
                ]
              },
              "scope": {
                "type": "string",
                "enum": [
                  "predict"
                ]
              },
              "window": {
                "type": "string",
                "enum": [
                  "minute",
                  "hour",
                  "day"
                ]
              },
              "retryAfterSeconds": {
                "type": "integer",
                "minimum": 1
              }
            }
          }
        ]
      },
      "LeafCare": {
        "type": "object",
        "required": [
          "watering",
          "sunlight",
          "soil"
        ],
        "properties": {
          "watering": {
            "type": "string"
          },
          "sunlight": {
            "type": "string"
          },
          "soil": {
            "type": "string"
          }
        }
      },
      "SimilarSpecies": {
        "type": "object",
        "required": [
          "commonName",
          "scientificName",
          "keyDifference"
        ],
        "properties": {
          "commonName": {
            "type": "string"
          },
          "scientificName": {
            "type": "string"
          },
          "keyDifference": {
            "type": "string"
          }
        }
      },
      "PredictSuccess": {
        "type": "object",
        "required": [
          "id",
          "leafName",
          "scientificName",
          "origin",
          "description",
          "accuracy",
          "accuracyPercent",
          "imageUrl",
          "timestamp",
          "confidence",
          "care",
          "toxicity",
          "petSafety",
          "seasonalInfo",
          "similarSpecies"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "leafName": {
            "type": "string"
          },
          "scientificName": {
            "type": "string"
          },
          "origin": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "accuracy": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ]
          },
          "accuracyPercent": {
            "type": "number"
          },
          "imageUrl": {
            "type": "string",
            "format": "uri"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "care": {
            "$ref": "#/components/schemas/LeafCare"
          },
          "toxicity": {
            "type": "string"
          },
          "petSafety": {
            "type": "string"
          },
          "seasonalInfo": {
            "type": "string"
          },
          "similarSpecies": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SimilarSpecies"
            },
            "minItems": 0,
            "maxItems": 3
          }
        }
      },
      "HistoryItem": {
        "type": "object",
        "required": [
          "id",
          "leafName",
          "scientificName",
          "origin",
          "description",
          "accuracy",
          "imageUrl",
          "timestamp"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "leafName": {
            "type": "string"
          },
          "scientificName": {
            "type": "string"
          },
          "origin": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "accuracy": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ]
          },
          "imageUrl": {
            "type": "string",
            "format": "uri"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DeleteHistoryResponse": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string",
            "enum": [
              "History item deleted."
            ]
          }
        }
      }
    }
  }
}
