{"openapi":"3.0.3","info":{"title":"CannaSage Developer API","version":"1.0","description":"The CannaSage Developer API provides programmatic access to cultivation data,\nSOPs, grow projects, environmental sensor readings, and data connectors.\n\n## Authentication\n\nAll endpoints require authentication via one of:\n\n| Method | Header | Use Case |\n|--------|--------|----------|\n| API Key | `X-API-Key: csk_live_...` | Server-to-server, partner integrations |\n| Bearer JWT | `Authorization: Bearer <token>` | Short-lived programmatic access |\n| Cookie | `accessToken` (httpOnly) | Browser sessions (internal) |\n\nAPI keys are created at `POST /api/v1/keys` using JWT authentication.\nKeys are shown **once** on creation — store them securely.\n\n## Scopes\n\nAPI keys are issued with specific scopes:\n\n| Scope | Access |\n|-------|--------|\n| `read` | Read SOPs, projects, environmental data, insights, grow tracking |\n| `connectors` | Manage connectors (create, update, delete, sync, list devices) |\n| `data:push` | Push data into connectors (for hardware partners) |\n\nJWT-authenticated users bypass scope checks entirely.\n\n## Rate Limits\n\nLimits are based on the owning user's subscription tier:\n\n| Tier | Requests | Window |\n|------|----------|--------|\n| Free | 100 | 15 min |\n| Premium | 500 | 15 min |\n| Enterprise | 5,000 | 15 min |\n\nRate limit headers are included in every response:\n- `X-RateLimit-Limit` — max requests in window\n- `X-RateLimit-Remaining` — requests remaining\n- `X-RateLimit-Reset` — UTC epoch when window resets\n\n## Response Format\n\n**Success:**\n```json\n{\n  \"data\": { ... },\n  \"meta\": { \"api_version\": \"v1\", \"total\": 42 }\n}\n```\n\n**Error:**\n```json\n{\n  \"error\": { \"code\": \"CONNECTOR_NOT_FOUND\", \"message\": \"...\", \"status\": 404 }\n}\n```\n\n## Versioning and deprecation\n\nThe API is versioned in the URL path (`/api/v1`). Breaking changes ship as a\nnew path prefix (`/api/v2`). Deprecated operations remain on v1 for at least\n90 days, are marked `deprecated: true` in this spec, and include `Deprecation`\nplus `Sunset` response headers when a removal date is set.\n","contact":{"name":"CannaSage Support","url":"https://cannasage.com/support"},"license":{"name":"Proprietary"}},"servers":[{"url":"/api/v1","description":"CannaSage API v1"}],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]},{"OAuth2":["read"]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Developer API key (format `csk_live_...`). Keys are issued with named\nscopes: `read`, `connectors`, `data:push`, and `ai:invoke`.\n"},"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"User session JWT or OAuth access token."},"OAuth2":{"type":"oauth2","description":"Scoped OAuth 2.0. Agents should request only the scopes they need\n(`read`, `connectors`, `data:push`, `ai:invoke`).\n","flows":{"clientCredentials":{"tokenUrl":"https://cannasage.app/oauth/token","scopes":{"read":"Read SOPs, projects, environmental data, insights, and grow tracking","connectors":"Create, update, delete, and sync data connectors","data:push":"Push hardware or sensor data into connectors","ai:invoke":"Invoke CannaSage AI analysis tools"}},"authorizationCode":{"authorizationUrl":"https://cannasage.app/oauth/authorize","tokenUrl":"https://cannasage.app/oauth/token","scopes":{"read":"Read SOPs, projects, environmental data, insights, and grow tracking","connectors":"Create, update, delete, and sync data connectors","data:push":"Push hardware or sensor data into connectors","ai:invoke":"Invoke CannaSage AI analysis tools"}}}}},"schemas":{"Meta":{"type":"object","properties":{"api_version":{"type":"string","example":"v1"},"total":{"type":"integer","example":42}}},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","example":"CONNECTOR_NOT_FOUND"},"message":{"type":"string","example":"Connector not found"},"status":{"type":"integer","example":404}}}}},"ApiKey":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"keyPrefix":{"type":"string","example":"csk_live_a1b2"},"scopes":{"type":"array","items":{"type":"string"},"example":["read","connectors"]},"lastUsedAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"SOP":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"category":{"type":"string"},"content":{"type":"string"},"authorId":{"type":"string"},"authorName":{"type":"string"},"visibility":{"type":"string","enum":["public","private","for_sale"]},"createdAt":{"type":"string","format":"date-time"}}},"Project":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"strain":{"type":"string","nullable":true},"status":{"type":"string","enum":["active","completed","archived"]},"createdAt":{"type":"string","format":"date-time"}}},"Connector":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"connectorType":{"type":"string","enum":["pulse_grow","trolmaster","growlink","csv_import","manual"]},"isActive":{"type":"boolean"},"status":{"type":"string"},"lastSyncAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"ConnectorData":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"connectorId":{"type":"string","format":"uuid"},"dataType":{"type":"string"},"rawData":{"type":"object"},"normalizedData":{"type":"object","nullable":true},"sourceTimestamp":{"type":"string","format":"date-time"},"externalId":{"type":"string","nullable":true}}},"EnvironmentalData":{"type":"object","properties":{"source":{"type":"string","example":"connector"},"connectorStatus":{"type":"string"},"lastSyncAt":{"type":"string","format":"date-time","nullable":true},"activeConnectorCount":{"type":"integer"},"latest":{"type":"object"},"historical":{"type":"array","items":{"type":"object"}},"rooms":{"type":"array","items":{"type":"string"}}}},"CultivationCost":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"category":{"type":"string"},"description":{"type":"string","nullable":true},"amount":{"type":"string"},"currency":{"type":"string","example":"USD"},"date":{"type":"string","format":"date"}}},"HarvestRecord":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"strain":{"type":"string","nullable":true},"yieldGrams":{"type":"string","nullable":true},"harvestDate":{"type":"string","format":"date"},"qualityScore":{"type":"integer","nullable":true}}},"Insight":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"severity":{"type":"string","enum":["high","medium","low"]},"category":{"type":"string"},"summary":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"ConnectorDevice":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","nullable":true}}},"SyncResult":{"type":"object","properties":{"success":{"type":"boolean"},"recordsImported":{"type":"integer"},"error":{"type":"string","nullable":true}}},"DeleteResult":{"type":"object","properties":{"success":{"type":"boolean"}}}}},"paths":{"/keys":{"post":{"operationId":"createApiKey","summary":"Create API key","description":"Generate a new API key. JWT auth only (API keys cannot create keys).","tags":["API Keys"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","example":"My Integration Key"},"scopes":{"type":"array","items":{"type":"string"},"default":["read"],"example":["read","connectors"]}}}}}},"responses":{"201":{"description":"Key created. The raw key is shown once.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"key":{"type":"string","example":"csk_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"},"prefix":{"type":"string","example":"csk_live_a1b2"},"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"message":{"type":"string"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Missing required field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key auth not allowed for this endpoint"}}},"get":{"operationId":"listApiKeys","summary":"List API keys","description":"List all API keys for the authenticated user. Hashes are never returned.","tags":["API Keys"],"responses":{"200":{"description":"List of keys","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}},"/keys/{id}":{"delete":{"operationId":"revokeApiKey","summary":"Revoke API key","description":"Revoke an active API key. JWT auth only.","tags":["API Keys"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Key revoked"},"404":{"description":"Key not found or already revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/sops":{"get":{"operationId":"listSops","summary":"List SOPs","description":"List Standard Operating Procedures with optional filters.","tags":["SOPs"],"parameters":[{"name":"category","in":"query","schema":{"type":"string"}},{"name":"search","in":"query","schema":{"type":"string"}},{"name":"authorId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"List of SOPs","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SOP"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}},"/sops/{id}":{"get":{"operationId":"getSop","summary":"Get SOP","description":"Get a specific SOP by ID.","tags":["SOPs"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"SOP details","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SOP"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"404":{"description":"SOP not found"}}}},"/projects":{"get":{"operationId":"listProjects","summary":"List projects","description":"List grow projects for the authenticated user.","tags":["Projects"],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["active","completed","archived"]}},{"name":"strain","in":"query","schema":{"type":"string"}},{"name":"search","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"List of projects","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}},"/projects/{id}":{"get":{"operationId":"getProject","summary":"Get project","description":"Get a specific project by ID.","tags":["Projects"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Project details","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Project"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"404":{"description":"Project not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/environmental/data":{"get":{"operationId":"listEnvironmentalData","summary":"Get environmental data","description":"Get environmental sensor data from connected devices.\n**Requires Premium or Enterprise subscription.**\n","tags":["Environmental"],"parameters":[{"name":"roomId","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":24}}],"responses":{"200":{"description":"Environmental data","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EnvironmentalData"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"403":{"description":"Premium subscription required"}}}},"/connectors":{"get":{"operationId":"listConnectors","summary":"List connectors","description":"List data connectors for the authenticated user.\n**Requires `connectors` scope and Premium+ subscription.**\n","tags":["Connectors"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]},{"OAuth2":["connectors"]}],"parameters":[{"name":"connectorType","in":"query","schema":{"type":"string","enum":["pulse_grow","trolmaster","growlink","csv_import","manual"]}},{"name":"isActive","in":"query","schema":{"type":"boolean"}},{"name":"projectId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"search","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"List of connectors","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Connector"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"403":{"description":"Insufficient scope or subscription tier","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"operationId":"createConnector","summary":"Create connector","description":"Create a new data connector.\n**Requires `connectors` scope and Premium+ subscription.**\n","tags":["Connectors"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]},{"OAuth2":["connectors"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","connectorType"],"properties":{"name":{"type":"string"},"connectorType":{"type":"string","enum":["pulse_grow","trolmaster","growlink","csv_import","manual"]},"config":{"type":"object","default":{}},"projectId":{"type":"string","format":"uuid"},"isActive":{"type":"boolean","default":true},"syncInterval":{"type":"integer","default":300,"description":"Sync interval in seconds"}}}}}},"responses":{"201":{"description":"Connector created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Connector"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/connectors/{id}":{"get":{"operationId":"getConnector","summary":"Get connector","description":"Get a data connector by ID, including type, status, and project assignment.","tags":["Connectors"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Connector details","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Connector"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"404":{"description":"Connector not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"operationId":"updateConnector","summary":"Update connector","description":"Update a connector's name, active flag, sync interval, or config.","tags":["Connectors"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"isActive":{"type":"boolean"},"syncInterval":{"type":"integer"},"config":{"type":"object"}}}}}},"responses":{"200":{"description":"Connector updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Connector"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"404":{"description":"Connector not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"operationId":"deleteConnector","summary":"Delete connector","description":"Permanently delete a data connector and its stored credentials.","tags":["Connectors"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Connector deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DeleteResult"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"404":{"description":"Connector not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/connectors/{id}/sync":{"post":{"operationId":"syncConnector","summary":"Trigger sync","description":"Trigger a manual data sync for a connector.","tags":["Connectors"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Sync result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SyncResult"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}},"/connectors/{id}/devices":{"get":{"operationId":"listConnectorDevices","summary":"List devices","description":"Get available devices from the external platform.","tags":["Connectors"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"List of devices","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ConnectorDevice"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"502":{"description":"External platform error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/connectors/{id}/data":{"get":{"operationId":"listConnectorData","summary":"Get connector data","description":"Get data records for a connector. Requires `read` scope.\n","tags":["Connectors"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"dataType","in":"query","schema":{"type":"string"}},{"name":"startDate","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"endDate","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","schema":{"type":"integer","default":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"processed","in":"query","schema":{"type":"boolean"},"description":"Filter by processing status"}],"responses":{"200":{"description":"Connector data records","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ConnectorData"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}},"post":{"operationId":"pushConnectorData","summary":"Push data","description":"Push data to a connector. For hardware partners.\n**Requires `data:push` scope.**\n","tags":["Connectors"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]},{"OAuth2":["data:push"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["dataType","rawData"],"properties":{"dataType":{"type":"string","example":"environmental"},"rawData":{"type":"object","example":{"temperature":75.2,"humidity":55,"co2":1200}},"normalizedData":{"type":"object"},"sourceTimestamp":{"type":"string","format":"date-time"},"externalId":{"type":"string"}}},"example":{"dataType":"environmental","rawData":{"temperature":75.2,"humidity":55,"co2":1200,"light_ppfd":800},"sourceTimestamp":"2026-02-15T12:00:00Z","externalId":"growlink-sensor-001-1708000000"}}}},"responses":{"201":{"description":"Data record created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ConnectorData"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Missing required fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Connector not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/insights":{"get":{"operationId":"listInsights","summary":"Get insights","description":"Get cultivation insights. Access depends on subscription tier:\n- **Free**: Daily synopsis only\n- **Premium/Enterprise**: Full categorized insights with severity levels\n","tags":["Insights"],"parameters":[{"name":"severity","in":"query","schema":{"type":"string","enum":["high","medium","low"]},"description":"Filter by severity (premium+ only)"}],"responses":{"200":{"description":"Insights data (format varies by tier)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Insight"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}},"/grow-tracking/costs":{"get":{"operationId":"listCultivationCosts","summary":"Get cultivation costs","description":"Get cultivation cost records for the authenticated user.","tags":["Grow Tracking"],"parameters":[{"name":"projectId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"category","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"List of costs","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CultivationCost"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}},"/grow-tracking/harvests":{"get":{"operationId":"listHarvestRecords","summary":"Get harvest records","description":"Get harvest records for the authenticated user.","tags":["Grow Tracking"],"parameters":[{"name":"projectId","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"List of harvests","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/HarvestRecord"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}}},"tags":[{"name":"API Keys","description":"Create, list, and revoke developer API keys"},{"name":"SOPs","description":"Standard Operating Procedures for cannabis cultivation"},{"name":"Projects","description":"Grow project management"},{"name":"Environmental","description":"Environmental sensor data from connected devices"},{"name":"Connectors","description":"Data connector management and sync"},{"name":"Insights","description":"AI-generated cultivation insights"},{"name":"Grow Tracking","description":"Cultivation cost tracking and harvest records"}]}