{
  "openapi": "3.1.0",
  "info": {
    "title": "Rootnotes API",
    "version": "1.0.0",
    "description": "REST API for managing plants, care events, photos, and insights. Authenticate with a Bearer token generated from Settings > Developer tokens."
  },
  "servers": [
    {
      "url": "https://rootnotes.app",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerToken": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "Developer token (prefix tnd_mcp_). Generate from Settings > Developer tokens."
      }
    },
    "schemas": {
      "Plant": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "userId": { "type": "string" },
          "name": { "type": "string" },
          "species": { "type": "string", "nullable": true },
          "location": { "type": "string", "nullable": true },
          "notes": { "type": "string", "nullable": true },
          "primaryPhotoId": { "type": "string", "format": "uuid", "nullable": true },
          "wateringFrequencyDays": { "type": "integer", "nullable": true },
          "wateringNotes": { "type": "string", "nullable": true },
          "lightRequirement": {
            "type": "string",
            "enum": ["low", "medium", "bright_indirect", "full_sun"],
            "nullable": true
          },
          "lightNotes": { "type": "string", "nullable": true },
          "soilType": { "type": "string", "nullable": true },
          "potType": {
            "type": "string",
            "enum": ["plastic", "terracotta", "ceramic", "fabric", "other"],
            "nullable": true
          },
          "fertilizerType": { "type": "string", "nullable": true },
          "fertilizerFrequencyDays": { "type": "integer", "nullable": true },
          "fertilizerNotes": { "type": "string", "nullable": true },
          "humidityPreference": {
            "type": "string",
            "enum": ["low", "medium", "high"],
            "nullable": true
          },
          "temperatureNotes": { "type": "string", "nullable": true },
          "mossPoleNeeded": { "type": "boolean" },
          "toxic": { "type": "boolean" },
          "careNotes": { "type": "string", "nullable": true },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" },
          "primaryPhoto": { "$ref": "#/components/schemas/Photo" },
          "lastWatered": { "type": "string", "format": "date-time", "nullable": true },
          "lastEvent": { "type": "string", "format": "date-time", "nullable": true }
        },
        "required": ["id", "userId", "name", "mossPoleNeeded", "toxic", "createdAt", "updatedAt"]
      },
      "CreatePlant": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 100 },
          "species": { "type": "string", "maxLength": 100 },
          "location": { "type": "string", "maxLength": 100 },
          "notes": { "type": "string", "maxLength": 1000 },
          "primaryPhotoId": { "type": "string", "format": "uuid" },
          "wateringFrequencyDays": { "type": "integer", "minimum": 1, "maximum": 365 },
          "wateringNotes": { "type": "string", "maxLength": 500 },
          "lightRequirement": { "type": "string", "enum": ["low", "medium", "bright_indirect", "full_sun"] },
          "lightNotes": { "type": "string", "maxLength": 500 },
          "soilType": { "type": "string", "maxLength": 100 },
          "potType": { "type": "string", "enum": ["plastic", "terracotta", "ceramic", "fabric", "other"] },
          "fertilizerType": { "type": "string", "maxLength": 100 },
          "fertilizerFrequencyDays": { "type": "integer", "minimum": 1, "maximum": 365 },
          "fertilizerNotes": { "type": "string", "maxLength": 500 },
          "humidityPreference": { "type": "string", "enum": ["low", "medium", "high"] },
          "temperatureNotes": { "type": "string", "maxLength": 500 },
          "mossPoleNeeded": { "type": "boolean", "default": false },
          "toxic": { "type": "boolean", "default": false },
          "careNotes": { "type": "string", "maxLength": 1000 }
        },
        "required": ["name"]
      },
      "UpdatePlant": {
        "type": "object",
        "description": "All fields optional. Only include fields you want to change.",
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 100 },
          "species": { "type": "string", "maxLength": 100, "nullable": true },
          "location": { "type": "string", "maxLength": 100, "nullable": true },
          "notes": { "type": "string", "maxLength": 1000, "nullable": true },
          "primaryPhotoId": { "type": "string", "format": "uuid", "nullable": true },
          "wateringFrequencyDays": { "type": "integer", "minimum": 1, "maximum": 365, "nullable": true },
          "lightRequirement": { "type": "string", "enum": ["low", "medium", "bright_indirect", "full_sun"], "nullable": true },
          "potType": { "type": "string", "enum": ["plastic", "terracotta", "ceramic", "fabric", "other"], "nullable": true },
          "humidityPreference": { "type": "string", "enum": ["low", "medium", "high"], "nullable": true },
          "mossPoleNeeded": { "type": "boolean" },
          "toxic": { "type": "boolean" }
        }
      },
      "Event": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "userId": { "type": "string" },
          "plantId": { "type": "string", "format": "uuid" },
          "type": {
            "type": "string",
            "enum": ["watered", "fertilized", "repotted", "soil_change", "note", "journal", "growth"]
          },
          "text": { "type": "string", "nullable": true },
          "growthAmount": { "type": "number", "nullable": true },
          "growthUnit": { "type": "string", "enum": ["cm", "in"], "nullable": true },
          "occurredAt": { "type": "string", "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" },
          "photos": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Photo" }
          }
        },
        "required": ["id", "userId", "plantId", "type", "occurredAt", "createdAt", "updatedAt"]
      },
      "CreateEvent": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": ["watered", "fertilized", "repotted", "soil_change", "note", "journal", "growth"],
            "default": "watered"
          },
          "text": { "type": "string", "maxLength": 2000 },
          "occurredAt": { "type": "string", "format": "date-time", "description": "Defaults to now" },
          "photoIds": { "type": "array", "items": { "type": "string", "format": "uuid" } },
          "isJournal": { "type": "boolean" },
          "growthAmount": { "type": "number", "exclusiveMinimum": 0 },
          "growthUnit": { "type": "string", "enum": ["cm", "in"] }
        }
      },
      "UpdateEvent": {
        "type": "object",
        "properties": {
          "text": { "type": "string", "maxLength": 2000, "nullable": true },
          "occurredAt": { "type": "string", "format": "date-time" },
          "photoIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Replaces all photos if provided" },
          "growthAmount": { "type": "number", "exclusiveMinimum": 0 },
          "growthUnit": { "type": "string", "enum": ["cm", "in"] }
        }
      },
      "Photo": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "userId": { "type": "string" },
          "plantId": { "type": "string", "format": "uuid", "nullable": true },
          "cloudinaryPublicId": { "type": "string" },
          "secureUrl": { "type": "string", "format": "uri" },
          "width": { "type": "integer" },
          "height": { "type": "integer" },
          "format": { "type": "string" },
          "bytes": { "type": "integer" },
          "cropX": { "type": "integer", "nullable": true },
          "cropY": { "type": "integer", "nullable": true },
          "cropWidth": { "type": "integer", "nullable": true },
          "cropHeight": { "type": "integer", "nullable": true },
          "createdAt": { "type": "string", "format": "date-time" }
        },
        "required": ["id", "userId", "cloudinaryPublicId", "secureUrl", "width", "height", "format", "bytes", "createdAt"]
      },
      "CreatePhoto": {
        "type": "object",
        "properties": {
          "plantId": { "type": "string", "format": "uuid" },
          "cloudinaryPublicId": { "type": "string", "minLength": 1 },
          "secureUrl": { "type": "string", "format": "uri" },
          "width": { "type": "integer", "minimum": 1 },
          "height": { "type": "integer", "minimum": 1 },
          "format": { "type": "string", "minLength": 1 },
          "bytes": { "type": "integer", "minimum": 0, "default": 0 }
        },
        "required": ["cloudinaryPublicId", "secureUrl", "width", "height", "format"]
      },
      "UpdatePhotoCrop": {
        "type": "object",
        "properties": {
          "x": { "type": "integer", "minimum": 0 },
          "y": { "type": "integer", "minimum": 0 },
          "width": { "type": "integer", "minimum": 1 },
          "height": { "type": "integer", "minimum": 1 }
        },
        "required": ["x", "y", "width", "height"],
        "description": "Crop must be approximately square (width and height within 2px) and within image bounds."
      },
      "EventList": {
        "type": "object",
        "properties": {
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/Event" } },
          "hasMore": { "type": "boolean" },
          "nextCursor": { "type": "string", "nullable": true }
        },
        "required": ["items", "hasMore"]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "code": { "type": "string" }
        },
        "required": ["error"]
      },
      "Success": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" }
        }
      }
    }
  },
  "paths": {
    "/api/plants": {
      "get": {
        "summary": "List all plants",
        "operationId": "listPlants",
        "tags": ["Plants"],
        "description": "Returns all plants for the authenticated user with last-watered and last-event timestamps.",
        "responses": {
          "200": {
            "description": "Plant list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/Plant" }
                }
              }
            }
          },
          "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Insufficient scopes", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "post": {
        "summary": "Create a plant",
        "operationId": "createPlant",
        "tags": ["Plants"],
        "description": "Create a new plant. Plant count is enforced by tier.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreatePlant" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Plant created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Plant" } } }
          },
          "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Insufficient scopes or plant limit reached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limited", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/plants/{id}": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
      ],
      "get": {
        "summary": "Get a plant",
        "operationId": "getPlant",
        "tags": ["Plants"],
        "responses": {
          "200": { "description": "Plant detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Plant" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "summary": "Update a plant",
        "operationId": "updatePlant",
        "tags": ["Plants"],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdatePlant" } } }
        },
        "responses": {
          "200": { "description": "Plant updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Plant" } } } },
          "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "summary": "Delete a plant",
        "operationId": "deletePlant",
        "tags": ["Plants"],
        "description": "Permanently deletes the plant and soft-deletes all associated events.",
        "responses": {
          "200": { "description": "Plant deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Success" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/plants/{id}/events": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "Plant ID" }
      ],
      "get": {
        "summary": "List events for a plant",
        "operationId": "listPlantEvents",
        "tags": ["Events"],
        "description": "Cursor-based pagination. Non-deleted events only.",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 15 } },
          { "name": "cursor", "in": "query", "schema": { "type": "string" }, "description": "Base64url cursor from previous response" }
        ],
        "responses": {
          "200": { "description": "Event list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EventList" } } } },
          "404": { "description": "Plant not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "post": {
        "summary": "Create an event",
        "operationId": "createEvent",
        "tags": ["Events"],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateEvent" } } }
        },
        "responses": {
          "201": { "description": "Event created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Event" } } } },
          "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Plant not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/plants/{id}/events/month": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
      ],
      "get": {
        "summary": "List events by month",
        "operationId": "listPlantEventsByMonth",
        "tags": ["Events"],
        "description": "Pro feature. Returns events within a date range.",
        "parameters": [
          { "name": "monthStart", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "Unix timestamp (ms)" },
          { "name": "monthEnd", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "Unix timestamp (ms)" }
        ],
        "responses": {
          "200": {
            "description": "Events in range",
            "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Event" } } } }
          },
          "400": { "description": "Missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Feature not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/events/{id}": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "Event ID" }
      ],
      "patch": {
        "summary": "Update an event",
        "operationId": "updateEvent",
        "tags": ["Events"],
        "description": "If photoIds is provided, it replaces all photos on the event.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateEvent" } } }
        },
        "responses": {
          "200": { "description": "Event updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Event" } } } },
          "404": { "description": "Not found or deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "summary": "Delete an event",
        "operationId": "deleteEvent",
        "tags": ["Events"],
        "description": "Soft-deletes the event (sets deletedAt).",
        "responses": {
          "200": { "description": "Event deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Success" } } } },
          "404": { "description": "Not found or already deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/photos": {
      "post": {
        "summary": "Create photo metadata",
        "operationId": "createPhoto",
        "tags": ["Photos"],
        "description": "Create photo metadata after uploading to Cloudinary. Photo limit enforced by tier.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatePhoto" } } }
        },
        "responses": {
          "201": { "description": "Photo created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Photo" } } } },
          "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Photo limit reached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/photos/{id}": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
      ],
      "get": {
        "summary": "Get a photo",
        "operationId": "getPhoto",
        "tags": ["Photos"],
        "responses": {
          "200": { "description": "Photo detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Photo" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "summary": "Update photo crop",
        "operationId": "updatePhotoCrop",
        "tags": ["Photos"],
        "description": "Update crop coordinates. Crop must be approximately square and within image bounds.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdatePhotoCrop" } } }
        },
        "responses": {
          "200": { "description": "Photo updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Photo" } } } },
          "400": { "description": "Invalid crop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/insights": {
      "get": {
        "summary": "Get analytics",
        "operationId": "getInsights",
        "tags": ["Insights"],
        "description": "Comprehensive analytics: care activity, species distribution, watering streaks, growth tracking.",
        "responses": {
          "200": {
            "description": "Analytics data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "overview": {
                      "type": "object",
                      "properties": {
                        "totalPlants": { "type": "integer" },
                        "totalEvents": { "type": "integer" },
                        "totalPhotos": { "type": "integer" },
                        "daysSinceFirstPlant": { "type": "integer", "nullable": true },
                        "mostActiveDay": { "type": "string", "nullable": true }
                      }
                    },
                    "careActivityByDay": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": { "type": "string" },
                          "type": { "type": "string" },
                          "count": { "type": "integer" }
                        }
                      }
                    },
                    "wateringStreaks": {
                      "type": "object",
                      "properties": {
                        "currentStreak": { "type": "integer" },
                        "longestStreak": { "type": "integer" },
                        "lastWateredDate": { "type": "string", "nullable": true }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/locations": {
      "get": {
        "summary": "List unique locations",
        "operationId": "listLocations",
        "tags": ["Plants"],
        "description": "Returns all unique non-empty location strings used in plants.",
        "responses": {
          "200": {
            "description": "Location list",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "type": "string" } }
              }
            }
          }
        }
      }
    },
    "/api/announcements": {
      "get": {
        "summary": "List active announcements",
        "operationId": "listAnnouncements",
        "tags": ["Announcements"],
        "description": "Returns currently active announcements. No specific scopes required.",
        "responses": {
          "200": {
            "description": "Announcement list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": { "type": "string", "format": "uuid" },
                      "textEn": { "type": "string" },
                      "textFr": { "type": "string" },
                      "linkUrl": { "type": "string", "nullable": true },
                      "linkTextEn": { "type": "string", "nullable": true },
                      "linkTextFr": { "type": "string", "nullable": true },
                      "active": { "type": "boolean" },
                      "startDate": { "type": "string", "format": "date-time", "nullable": true },
                      "endDate": { "type": "string", "format": "date-time", "nullable": true }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/mcp": {
      "get": {
        "summary": "MCP server info",
        "operationId": "getMcpInfo",
        "tags": ["MCP"],
        "security": [],
        "description": "Returns MCP server capabilities, available scopes, and tool list. No authentication required.",
        "responses": {
          "200": {
            "description": "MCP info",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": { "type": "string" },
                    "version": { "type": "string" },
                    "auth": { "type": "string" },
                    "scopes": { "type": "array", "items": { "type": "string" } },
                    "tools": { "type": "array", "items": { "type": "string" } }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "MCP Streamable HTTP",
        "operationId": "mcpStreamableHttp",
        "tags": ["MCP"],
        "description": "MCP Streamable HTTP endpoint. Accepts JSON-RPC 2.0 requests for tool invocation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "jsonrpc": { "type": "string", "const": "2.0" },
                  "method": { "type": "string" },
                  "params": { "type": "object" },
                  "id": { "oneOf": [{ "type": "string" }, { "type": "integer" }] }
                },
                "required": ["jsonrpc", "method", "id"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "JSON-RPC response" },
          "401": { "description": "Missing Bearer token" },
          "403": { "description": "Invalid, revoked, or expired token" }
        }
      }
    }
  },
  "tags": [
    { "name": "Plants", "description": "Plant CRUD and locations" },
    { "name": "Events", "description": "Care events (watered, fertilized, etc.)" },
    { "name": "Photos", "description": "Photo metadata (images stored in Cloudinary)" },
    { "name": "Insights", "description": "Analytics and statistics" },
    { "name": "Announcements", "description": "App announcements" },
    { "name": "MCP", "description": "Model Context Protocol endpoint for AI assistants" }
  ]
}
