{"openapi":"3.1.0","info":{"title":"DataKubo API","description":"Water monitoring API for IoT devices","version":"1.0.0"},"paths":{"/api/v1/ingest/water-consumption":{"post":{"tags":["ingestion"],"summary":"Ingest Water Consumption","description":"Unified webhook ingestion endpoint for water consumption data from multiple LoRaWAN network servers.\n\n**Supported Formats:**\n- ChirpStack (automatic detection)\n- The Things Network (TTN) v3 (automatic detection)\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n**Rate Limit:** 100,000 requests per month per reseller\n**Integration Time:** ~15 minutes with ChirpStack or TTN\n\n**Format Detection:** Automatic - no format parameter required\n- ChirpStack: Detected by presence of 'deviceInfo' and 'object' keys\n- TTN: Detected by presence of 'end_device_ids' and 'uplink_message' keys\n\n**Request Body (ChirpStack Format):**\n```json\n{\n  \"deviceInfo\": {\n    \"tenantName\": \"my-community\",\n    \"deviceName\": \"383936306C4B5880 24h\",\n    \"devEui\": \"383936306c4b5880\"\n  },\n  \"time\": \"2025-08-04T20:34:35.816710+00:00\",\n  \"object\": {\n    \"cumulativeFlowM3\": 0.019999999552965164,\n    \"batteryVoltage\": 3.658116102218628,\n    \"latitude\": 40.416775,\n    \"longitude\": -3.703790,\n    \"altitude\": 667.5\n  }\n}\n```\n\n**Request Body (TTN Format):**\n```json\n{\n  \"end_device_ids\": {\n    \"dev_eui\": \"383936306c4b5880\",\n    \"application_ids\": {\n      \"application_id\": \"my-community\"\n    }\n  },\n  \"uplink_message\": {\n    \"decoded_payload\": {\n      \"cumulativeFlowM3\": 0.02,\n      \"battery\": 85.5\n    }\n  },\n  \"received_at\": \"2025-08-04T20:34:35.816710+00:00\"\n}\n```\n\n**Response Codes:**\n- 200: Data ingested successfully\n- 400: Invalid request payload or unknown format\n- 401: Invalid or missing API key\n- 403: Tenant name mismatch\n- 404: Device not found\n- 429: Rate limit exceeded (100,000 requests/month)\n\n**Success Response:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"dev_eui\": \"383936306c4b5880\",\n    \"timestamp\": \"2025-08-04T20:34:35.816710+00:00\",\n    \"consumption_liters\": 20.0,\n    \"battery_voltage\": 3.658116102218628,\n    \"format\": \"chirpstack\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Data ingested successfully\"\n}\n```\n\n**Error Response (Unknown Format):**\n```json\n{\n  \"status\": \"error\",\n  \"error_code\": \"INVALID_PAYLOAD_FORMAT\",\n  \"message\": \"Unknown webhook format - expected ChirpStack or TTN\",\n  \"details\": {\n    \"provided_keys\": [\"some\", \"keys\"],\n    \"expected_chirpstack\": [\"deviceInfo\", \"object\"],\n    \"expected_ttn\": [\"end_device_ids\", \"uplink_message\"]\n  }\n}\n```\n\n**Integration Guide:**\n- ChirpStack: Configure this endpoint in HTTP integration settings\n- TTN: Configure this endpoint as webhook integration\n- Each customer can use the same endpoint - we route by tenant_name automatically","operationId":"ingest_water_consumption_api_v1_ingest_water_consumption_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/register":{"post":{"tags":["devices"],"summary":"Register Device","description":"Register a new LoRaWAN water monitoring device with optional hierarchy support\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**LoRaWAN Device EUI:**\nThe Device EUI (devEui) is a globally unique 64-bit identifier assigned to each\nLoRaWAN device by the manufacturer. It must be 16 hexadecimal characters.\n\n**Global Uniqueness:**\nDevice EUIs are globally unique across ALL resellers. Once a device is\nregistered by any reseller, that devEui cannot be used again. This prevents\nconflicts and ensures proper device ownership.\n\n**Hierarchy Fields (Optional):**\n- parent_device_id: UUID of parent device (null for independent devices)\n- is_principal: Boolean flag for principal/root devices (default: false)\n\nDevices default to no parent (standalone operation). Hierarchy can be used for:\n- Principal meters with sub-meters\n- Building zones with devices\n- Equipment groups\n- Sensor networks\n- Any parent-child device relationship\n\n**Request Body (Basic):**\n```json\n{\n  \"dev_eui\": \"383936306c4b5880\",\n  \"name\": \"Water Meter 001\",\n  \"device_type\": \"water_meter\"\n}\n```\n\n**Request Body (With Hierarchy):**\n```json\n{\n  \"dev_eui\": \"383936306c4b5881\",\n  \"name\": \"Sub-Meter 001\",\n  \"device_type\": \"water_meter\",\n  \"parent_device_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n  \"is_principal\": false\n}\n```\n\n**Request Body (Principal Device):**\n```json\n{\n  \"dev_eui\": \"383936306c4b5882\",\n  \"name\": \"Main Building Meter\",\n  \"device_type\": \"water_meter\",\n  \"is_principal\": true\n}\n```\n\n**Response Codes:**\n- 200: Device registered successfully\n- 400: Device already exists (DEVICE_ALREADY_EXISTS)\n- 400: Hierarchy too deep (HIERARCHY_TOO_DEEP)\n- 404: Parent device not found (PARENT_DEVICE_NOT_FOUND)\n- 401: Invalid or missing API key\n- 422: Invalid payload (devEui format incorrect)\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"dev_eui\": \"383936306c4b5880\",\n    \"name\": \"Water Meter 001\",\n    \"device_name\": null,\n    \"device_type\": \"water_meter\",\n    \"status\": \"offline\",\n    \"battery_level\": null,\n    \"last_reading_at\": null,\n    \"is_active\": true,\n    \"parent_device_id\": null,\n    \"is_principal\": false,\n    \"hierarchy_level\": 0,\n    \"deleted_at\": null,\n    \"created_at\": \"2025-01-01T00:00:00+00:00\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Device registered successfully\"\n}\n```\n\n**Hierarchy Validation:**\nWhen parent_device_id is provided, the system validates:\n- Parent device exists and belongs to same reseller\n- Parent device is not soft-deleted\n- Hierarchy depth does not exceed 10 levels\n- hierarchy_level is automatically calculated (parent's level + 1)\n\n**Integration Notes:**\n- Register devices BEFORE they start sending data via ChirpStack webhook\n- Device status starts as \"offline\" and updates to \"online\" on first data ingestion\n- device_name field is populated automatically from ChirpStack webhook data\n- Only is_active=true AND deleted_at IS NULL devices count toward billing\n- Hierarchy is optional - devices work independently by default\n\n**Error Codes:**\n- DEVICE_ALREADY_EXISTS: Device with this dev_eui already registered\n- PARENT_DEVICE_NOT_FOUND: Specified parent device doesn't exist\n- HIERARCHY_TOO_DEEP: Hierarchy would exceed 10 levels\n\nRequirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 6.1, 6.2, 6.3, 6.4, 6.5, 6.7","operationId":"register_device_api_v1_devices_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceRegisterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices":{"get":{"tags":["devices"],"summary":"List Devices","description":"List all devices for the authenticated reseller with optional filtering\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Multi-Tenant Filtering:**\nThis endpoint automatically filters devices by the authenticated reseller.\nEach reseller can only see their own devices, ensuring complete data isolation.\n\n**Query Parameters:**\n- is_principal (optional): Filter by principal device flag\n  - true: Return only principal devices (root devices in hierarchy)\n  - false: Return only non-principal devices (sub-devices)\n  - omitted: Return all devices\n- include_consumption (optional): Include consumption data for sparklines (default: true)\n  - true: Include last_consumption_m3 and consumption_trend fields\n  - false: Skip consumption data for faster response\n- customer_id (optional): Filter devices by customer UUID (legacy parameter)\n- customer (optional): Filter by customer association\n  - UUID: Return only devices assigned to end users belonging to this customer\n  - \"unassociated\": Return only devices without customer assignment (customer_id IS NULL)\n  - omitted: Return all devices for the reseller\n- user (optional): Filter by user assignment status\n  - \"assigned\": Return only devices with end_user_id assigned\n  - \"unassigned\": Return only devices without end_user_id (end_user_id IS NULL)\n  - omitted: Return all devices\n- first_seen (optional): Filter by first_seen_at timestamp\n  - \"today\": Devices first seen today\n  - \"week\": Devices first seen in the last 7 days\n  - \"month\": Devices first seen in the last 30 days\n  - ISO date (YYYY-MM-DD): Devices first seen on or after this date\n  - omitted: Return all devices\n- search (optional): Search by dev_eui or device_name (case-insensitive partial match)\n\n**Filtering Examples:**\n```bash\n# Get all devices with consumption data\nGET /api/v1/devices\n\n# Get only principal meters (root devices)\nGET /api/v1/devices?is_principal=true\n\n# Get unassociated devices (no customer)\nGET /api/v1/devices?customer=unassociated\n\n# Get devices without assigned users\nGET /api/v1/devices?user=unassigned\n\n# Get new devices from last 7 days\nGET /api/v1/devices?first_seen=week\n\n# Search devices by EUI or name\nGET /api/v1/devices?search=383936\n\n# Combine filters (AND logic)\nGET /api/v1/devices?customer=unassociated&first_seen=week\n```\n\n**Response Format:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"dev_eui\": \"383936306c4b5880\",\n      \"device_name\": \"383936306C4B5880 24h\",\n      \"name\": \"Water Meter 001\",\n      \"device_type\": \"water_meter\",\n      \"status\": \"online\",\n      \"battery_level\": 3.65,\n      \"last_reading_at\": \"2025-01-01T12:00:00+00:00\",\n      \"is_active\": true,\n      \"end_user_id\": \"uuid\",\n      \"customer_id\": \"uuid\",\n      \"customer_name\": \"Building A\",\n      \"first_seen_at\": \"2025-01-01T00:00:00+00:00\",\n      \"last_seen_at\": \"2025-01-01T12:00:00+00:00\",\n      \"message_count\": 145,\n      \"parent_device_id\": null,\n      \"is_principal\": true,\n      \"hierarchy_level\": 0,\n      \"last_consumption_m3\": 0.125,\n      \"consumption_trend\": [0.08, 0.12, 0.09, 0.15, 0.11, 0.10, 0.125],\n      \"deleted_at\": null,\n      \"created_at\": \"2025-01-01T00:00:00+00:00\"\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success (returns empty array if no devices exist)\n- 401: Invalid or missing API key\n\n**Filter Behavior:**\n- All filters combine with AND logic\n- URL parameters are reflected in the response for debugging\n- Empty results return empty array with status \"success\"\n\nRequirements: 2.1-2.5, 7.4, 7.6, 7.7, 7.13, 9.4, 9.5, 10.2","operationId":"list_devices_api_v1_devices_get","parameters":[{"name":"is_principal","in":"query","required":false,"schema":{"type":"boolean","title":"Is Principal"}},{"name":"include_consumption","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Include Consumption"}},{"name":"customer_id","in":"query","required":false,"schema":{"type":"string","title":"Customer Id"}},{"name":"customer","in":"query","required":false,"schema":{"type":"string","title":"Customer"}},{"name":"user","in":"query","required":false,"schema":{"type":"string","title":"User"}},{"name":"first_seen","in":"query","required":false,"schema":{"type":"string","title":"First Seen"}},{"name":"search","in":"query","required":false,"schema":{"type":"string","title":"Search"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/stats":{"get":{"tags":["devices"],"summary":"Get Device Stats","description":"Get device statistics for KPI cards on the devices page\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Multi-Tenant Filtering:**\nThis endpoint automatically filters devices by the authenticated reseller.\nEach reseller can only see statistics for their own devices.\n\n**Query Parameters:**\n- customer_id (optional): Filter statistics by customer\n  - UUID: Return stats only for devices assigned to end users belonging to this customer\n  - omitted: Return stats for all devices for the reseller\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"total\": 156,\n    \"new_7_days\": 12,\n    \"unassociated\": 8,\n    \"unassigned_users\": 23\n  },\n  \"status\": \"success\"\n}\n```\n\n**Statistics Explained:**\n- total: Total number of active (non-deleted) devices\n- new_7_days: Devices with first_seen_at within the last 7 days\n- unassociated: Devices without a customer assignment (customer_id IS NULL via end_user)\n- unassigned_users: Devices without an end user assignment (end_user_id IS NULL)\n\n**Response Codes:**\n- 200: Success\n- 401: Invalid or missing authentication\n\n**Use Cases:**\n- Display KPI cards on /admin/devices page\n- Show fleet status at a glance\n- Identify devices needing attention (unassociated, unassigned)\n- Track new device registrations\n\nRequirements: 2.7-2.26 (KPI Cards)","operationId":"get_device_stats_api_v1_devices_stats_get","parameters":[{"name":"customer_id","in":"query","required":false,"schema":{"type":"string","title":"Customer Id"}},{"name":"customer","in":"query","required":false,"schema":{"type":"string","title":"Customer"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{dev_eui}":{"get":{"tags":["devices"],"summary":"Get Device By Eui","description":"Lookup device by Device EUI for commissioning and troubleshooting\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Device EUI Lookup:**\nThis endpoint retrieves complete device information using the LoRaWAN Device EUI.\nIt's particularly useful during device commissioning, troubleshooting, and when\nyou need to quickly look up a device without knowing its internal UUID.\n\n**Case-Insensitive Lookup:**\nThe Device EUI lookup is case-insensitive. All of these are equivalent:\n- 383936306c4b5880 (lowercase)\n- 383936306C4B5880 (uppercase)\n- 383936306C4b5880 (mixed case)\n\n**Path Parameters:**\n- dev_eui: LoRaWAN Device EUI (16 hex characters, case-insensitive)\n\n**Response Codes:**\n- 200: Device found, returns complete device data\n- 404: Device not found or soft-deleted (DEVICE_NOT_FOUND)\n- 401: Invalid or missing API key\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"dev_eui\": \"383936306c4b5880\",\n    \"device_name\": \"383936306C4B5880 24h\",\n    \"name\": \"Water Meter 001\",\n    \"device_type\": \"water_meter\",\n    \"status\": \"online\",\n    \"battery_level\": 3.65,\n    \"last_reading_at\": \"2025-01-01T12:00:00+00:00\",\n    \"is_active\": true,\n    \"end_user_id\": \"uuid\",\n    \"parent_device_id\": null,\n    \"is_principal\": true,\n    \"hierarchy_level\": 0,\n    \"deleted_at\": null,\n    \"created_at\": \"2025-01-01T00:00:00+00:00\"\n  },\n  \"status\": \"success\"\n}\n```\n\n**Multi-Tenant Security:**\n- Only devices belonging to the authenticated reseller are returned\n- Cross-reseller device lookups are prevented automatically\n- Device EUI must match AND belong to the authenticated reseller\n\n**Soft Delete:**\n- Soft-deleted devices (deleted_at IS NOT NULL) return 404\n- Only active devices (deleted_at IS NULL) are returned\n\n**Use Cases:**\n- Quick device lookup during field commissioning\n- Troubleshooting device connectivity issues\n- Verifying device registration before data ingestion\n- Looking up device details when you only have the Device EUI from hardware\n- Checking device status during installation\n\n**Integration Notes:**\n- Device EUI is the same identifier used in ChirpStack webhook payloads\n- Use this endpoint to verify device registration before configuring webhooks\n- Useful for installer technicians who have physical access to devices\n\nRequirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 10.2","operationId":"get_device_by_eui_api_v1_devices__dev_eui__get","parameters":[{"name":"dev_eui","in":"path","required":true,"schema":{"type":"string","title":"Dev Eui"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}":{"put":{"tags":["devices"],"summary":"Update Device","description":"Update device details (name, device_name, parent_device_id)\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Path Parameters:**\n- device_id: Device UUID\n\n**Request Body:**\n```json\n{\n  \"name\": \"Water Meter 001 - Kitchen\",\n  \"device_name\": \"383936306C4B5880 24h\",\n  \"parent_device_id\": \"123e4567-e89b-12d3-a456-426614174000\"\n}\n```\n\n**Response Codes:**\n- 200: Device updated successfully\n- 400: Invalid request (validation error)\n- 404: Device not found (DEVICE_NOT_FOUND)\n- 401: Invalid or missing authentication\n\n**Multi-Tenant Security:**\n- Device must belong to the authenticated reseller\n- Cross-reseller updates are prevented automatically","operationId":"update_device_api_v1_devices__device_id__put","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["devices"],"summary":"Delete Device","description":"Soft delete a device (preserves historical data and relationships)\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Soft Delete:**\nThis endpoint performs a \"soft delete\" by setting the deleted_at timestamp\ninstead of removing the database record. This approach:\n- Preserves all historical consumption data\n- Maintains device relationships (children remain active)\n- Allows device restoration if deleted by mistake\n- Stops billing immediately (is_active set to false)\n- Excludes device from normal queries\n\n**Billing Impact:**\nSoft-deleted devices are immediately excluded from billing calculations.\nThe device's is_active flag is automatically set to false, ensuring it\nno longer counts toward the monthly device count.\n\n**Path Parameters:**\n- device_id: UUID of the device to delete\n\n**Response Codes:**\n- 200: Device soft-deleted successfully\n- 404: Device not found or already deleted (DEVICE_NOT_FOUND)\n- 401: Invalid or missing API key\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"device_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n    \"deleted_at\": \"2025-01-01T12:00:00+00:00\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Device soft-deleted successfully. Historical data preserved.\"\n}\n```\n\n**Child Device Behavior:**\nWhen a parent device is soft-deleted:\n- Children remain active (deleted_at stays NULL)\n- Children's parent_device_id remains intact (points to deleted parent)\n- Children become \"orphaned\" but continue functioning normally\n- Children can still be queried via GET /devices/{parent_id}/children\n\n**Historical Data:**\nAll consumption data for the deleted device is preserved in the database.\nThis ensures:\n- Historical reports remain accurate\n- Billing disputes can be resolved with complete data\n- Device can be restored with full history intact\n\n**ChirpStack Integration:**\nIf ChirpStack continues sending data for a soft-deleted device:\n- Data is accepted and stored (200 response)\n- Warning is logged for investigation\n- Response includes device_deleted=true flag\n\n**Multi-Tenant Security:**\n- Only devices belonging to the authenticated reseller can be deleted\n- Cross-reseller deletions are prevented automatically\n\n**Restoration:**\nSoft-deleted devices can be restored using PUT /devices/{device_id}/restore.\nThis reverses the soft delete, setting deleted_at back to NULL and\nis_active back to true.\n\n**Use Cases:**\n- Decommission devices while preserving historical data\n- Remove devices from billing without losing consumption records\n- Temporarily hide devices from active inventory\n- Clean up test devices while maintaining audit trail\n\n**Important Notes:**\n- Soft-deleted devices do not appear in GET /devices list\n- Soft-deleted devices return 404 on GET /devices/{dev_eui}\n- Soft-deleted devices cannot be assigned, updated, or have parent changed\n- To permanently remove a device, contact support (hard delete requires manual intervention)\n\nRequirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7, 18.1","operationId":"delete_device_api_v1_devices__device_id__delete","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{dev_eui}/assign":{"put":{"tags":["devices"],"summary":"Assign Device","description":"Assign a device to an end user or unassign it\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Device-to-User Assignment:**\nThis endpoint allows reseller admins to assign water monitoring devices\nto specific end users. Once assigned, the end user can view the device's\nconsumption data in their personal dashboard.\n\n**Ownership History Tracking:**\nWhen a device is reassigned from one user to another, the system automatically:\n- Creates an ownership history record\n- Generates a final consumption report for the previous owner\n- Stores the report in Supabase Storage\n\n**Unassignment:**\nTo unassign a device from a user, pass `end_user_id: null` in the request body.\nThis removes the device from the user's dashboard without deleting the device.\n\n**Customer Preservation on Unassignment:**\nWhen unassigning a device from a user, the customer association is automatically\npreserved. If the device's customer was derived from the user (via end_user.customer_id),\nthe customer_id is copied directly to the device before removing the user.\nThis ensures the device remains associated with the customer even without a user.\n\n**Path Parameters:**\n- dev_eui: LoRaWAN Device EUI (16 hex characters)\n\n**Request Body:**\n```json\n{\n  \"end_user_id\": \"123e4567-e89b-12d3-a456-426614174000\"\n}\n```\n\n**Unassignment Example:**\n```json\n{\n  \"end_user_id\": null\n}\n```\n\n**Response Codes:**\n- 200: Device assigned/unassigned successfully\n- 404: Device not found (DEVICE_NOT_FOUND)\n- 404: End user not found (USER_NOT_FOUND)\n- 401: Invalid or missing API key\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"dev_eui\": \"383936306c4b5880\",\n    \"device_name\": \"383936306C4B5880 24h\",\n    \"name\": \"Water Meter 001\",\n    \"device_type\": \"water_meter\",\n    \"status\": \"online\",\n    \"battery_level\": 3.65,\n    \"last_reading_at\": \"2025-01-01T12:00:00+00:00\",\n    \"is_active\": true,\n    \"end_user_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n    \"parent_device_id\": null,\n    \"is_principal\": false,\n    \"hierarchy_level\": 0,\n    \"deleted_at\": null,\n    \"created_at\": \"2025-01-01T00:00:00+00:00\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Device assigned successfully\"\n}\n```\n\n**Multi-Tenant Security:**\n- Devices can only be assigned to users within the same reseller\n- Cross-reseller assignments are prevented automatically\n- Both device and user must belong to the authenticated reseller\n\n**Hierarchy Preservation:**\nDevice assignment only updates the end_user_id field. All hierarchy fields\n(parent_device_id, is_principal, hierarchy_level) are preserved during assignment.\nThis ensures that device relationships remain intact when reassigning devices.\n\n**Soft Delete:**\nSoft-deleted devices (deleted_at IS NOT NULL) cannot be assigned and will\nreturn 404 DEVICE_NOT_FOUND. Restore the device first before assigning.\n\n**Use Cases:**\n- Assign newly registered device to a specific household/user\n- Reassign device when user moves or changes\n- Unassign device for maintenance or redeployment\n\nRequirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 17.1, 19.1, 19.2, 19.3, 19.4, 19.5, 19.7, 19.10","operationId":"assign_device_api_v1_devices__dev_eui__assign_put","parameters":[{"name":"dev_eui","in":"path","required":true,"schema":{"type":"string","title":"Dev Eui"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceAssignRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}/status":{"put":{"tags":["devices"],"summary":"Update Device Status","description":"Manually set device to maintenance mode or return to automatic status management\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Manual Status Management:**\nThis endpoint allows reseller admins to manually override automatic status management.\nThe primary use case is setting devices to \"maintenance\" mode to prevent automatic\nstatus changes during repairs or servicing.\n\n**Status Values (Manual Only):**\n- \"maintenance\": Manually set device to maintenance mode (prevents automatic online/offline updates)\n- \"auto\": Return device to automatic status management (calculates status based on last_reading_at)\n\n**Automatic Status Calculation:**\nWhen status=\"auto\", the system calculates the device status based on last_reading_at:\n- Online: last_reading_at < 48 hours ago (accommodates devices sending every 24-36h)\n- Offline: last_reading_at >= 48 hours ago OR last_reading_at is NULL\n\n**Important Notes:**\n- \"online\" and \"offline\" statuses are managed automatically by the system\n- Attempting to manually set \"online\" or \"offline\" will return 400 error\n- Use \"maintenance\" for manual control, \"auto\" to restore automatic management\n- The ingestion endpoint automatically sets status to \"online\" when data is received\n  (unless device is in \"maintenance\" mode)\n\n**Billing Impact:**\nThe `is_active` field directly affects billing calculations. Only devices with\n`is_active=true` count toward the monthly device count. Use this field to:\n- Deactivate devices during extended maintenance periods\n- Remove decommissioned devices from billing without deleting them\n- Temporarily exclude devices from billing during troubleshooting\n\n**Path Parameters:**\n- device_id: Device UUID (not dev_eui)\n\n**Request Body (Maintenance Mode):**\n```json\n{\n  \"status\": \"maintenance\",\n  \"is_active\": false\n}\n```\n\n**Request Body (Return to Auto):**\n```json\n{\n  \"status\": \"auto\"\n}\n```\n\n**Optional Fields:**\n- is_active: If omitted, the current value is preserved\n\n**Response Codes:**\n- 200: Device status updated successfully\n- 400: Invalid status value (INVALID_STATUS) - attempted to set \"online\" or \"offline\"\n- 404: Device not found (DEVICE_NOT_FOUND)\n- 401: Invalid or missing API key\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"dev_eui\": \"383936306c4b5880\",\n    \"device_name\": \"383936306C4B5880 24h\",\n    \"name\": \"Water Meter 001\",\n    \"device_type\": \"water_meter\",\n    \"status\": \"maintenance\",\n    \"battery_level\": 3.65,\n    \"last_reading_at\": \"2025-01-01T12:00:00+00:00\",\n    \"is_active\": false,\n    \"end_user_id\": \"uuid\",\n    \"parent_device_id\": null,\n    \"is_principal\": false,\n    \"hierarchy_level\": 0,\n    \"deleted_at\": null,\n    \"created_at\": \"2025-01-01T00:00:00+00:00\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Device status updated to maintenance\"\n}\n```\n\n**Multi-Tenant Security:**\n- Only devices belonging to the authenticated reseller can be updated\n- Cross-reseller status updates are prevented automatically\n\n**Hierarchy Preservation:**\nDevice status updates only modify the status and is_active fields. All hierarchy\nfields (parent_device_id, is_principal, hierarchy_level) are preserved during\nstatus updates. This ensures that device relationships remain intact.\n\n**Soft Delete:**\nSoft-deleted devices (deleted_at IS NOT NULL) cannot have their status updated\nand will return 404 DEVICE_NOT_FOUND. Restore the device first before updating status.\n\n**Use Cases:**\n- Mark device as \"maintenance\" when performing repairs (prevents auto-status changes)\n- Set is_active=false to exclude from billing during extended downtime\n- Return device to \"auto\" after maintenance completion\n- Temporarily override automatic status management\n\nRequirements: 4b.1, 4b.2, 4b.3, 4b.4, 4b.5, 4b.6, 4b.7, 4b.8","operationId":"update_device_status_api_v1_devices__device_id__status_put","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceStatusRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}/parent":{"put":{"tags":["devices"],"summary":"Set Device Parent","description":"Set or remove device parent for hierarchy management\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Device Hierarchy:**\nThis endpoint allows reseller admins to organize devices in a parent-child\nhierarchy. Hierarchies can be used for various organizational structures:\n- Principal meters with sub-meters\n- Building zones with devices\n- Equipment groups\n- Sensor networks\n- Any parent-child device relationship\n\n**Setting a Parent:**\nTo add a device to a hierarchy, provide the parent device's UUID in the\nrequest body. The system will automatically:\n- Validate the parent exists and belongs to the same reseller\n- Check for circular references (device cannot be its own ancestor)\n- Calculate the new hierarchy level (parent's level + 1)\n- Enforce depth limit (max 10 levels)\n- Update all descendant devices' hierarchy levels\n\n**Removing a Parent:**\nTo remove a device from a hierarchy and make it independent, pass\n`parent_device_id: null` in the request body. The system will:\n- Set the device's hierarchy level to 0\n- Recalculate hierarchy levels for all child devices\n- Preserve all child relationships (children remain attached)\n\n**Path Parameters:**\n- device_id: UUID of the device to update\n\n**Request Body:**\n```json\n{\n  \"parent_device_id\": \"123e4567-e89b-12d3-a456-426614174000\"\n}\n```\n\n**Remove Parent Example:**\n```json\n{\n  \"parent_device_id\": null\n}\n```\n\n**Response Codes:**\n- 200: Parent updated successfully\n- 404: Device not found (DEVICE_NOT_FOUND)\n- 404: Parent device not found (PARENT_DEVICE_NOT_FOUND)\n- 400: Circular reference detected (CIRCULAR_REFERENCE)\n- 400: Hierarchy too deep (HIERARCHY_TOO_DEEP)\n- 401: Invalid or missing API key\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"dev_eui\": \"383936306c4b5880\",\n    \"device_name\": \"383936306C4B5880 24h\",\n    \"name\": \"Water Meter 001\",\n    \"device_type\": \"water_meter\",\n    \"status\": \"online\",\n    \"battery_level\": 3.65,\n    \"last_reading_at\": \"2025-01-01T12:00:00+00:00\",\n    \"is_active\": true,\n    \"end_user_id\": \"uuid\",\n    \"parent_device_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n    \"is_principal\": false,\n    \"hierarchy_level\": 1,\n    \"deleted_at\": null,\n    \"created_at\": \"2025-01-01T00:00:00+00:00\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Device parent updated successfully\"\n}\n```\n\n**Validation Rules:**\n- Parent device must exist and belong to the same reseller\n- Parent device cannot be soft-deleted (deleted_at IS NULL)\n- Setting parent cannot create circular references\n- Hierarchy depth cannot exceed 10 levels\n- Device cannot be its own parent or ancestor\n\n**Multi-Tenant Security:**\n- Both device and parent must belong to the authenticated reseller\n- Cross-reseller hierarchy relationships are prevented automatically\n\n**Hierarchy Recalculation:**\nWhen a device's parent changes, the system automatically recalculates\nhierarchy levels for all descendant devices to maintain consistency.\nThis ensures the entire subtree reflects the correct depth.\n\n**Use Cases:**\n- Organize principal meters with sub-meters for consumption rollup\n- Group devices by building zones or floors\n- Create equipment hierarchies (controller → sensors)\n- Model sensor networks with gateway devices\n- Reorganize device structure without re-registering devices\n\nRequirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8","operationId":"set_device_parent_api_v1_devices__device_id__parent_put","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetParentRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}/children":{"get":{"tags":["devices"],"summary":"Get Device Children","description":"Get child devices in the hierarchy\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Device Hierarchy Queries:**\nThis endpoint retrieves child devices for a given parent device. It supports\ntwo modes: direct children only, or all descendants (recursive).\n\n**Direct Children (default):**\nReturns only the immediate children of the specified device. This is useful\nfor displaying one level of the hierarchy at a time, or for building\nexpandable tree views.\n\n**All Descendants (recursive):**\nWhen `include_descendants=true`, returns all devices in the subtree below\nthe specified device, regardless of depth. This is useful for:\n- Calculating total consumption across an entire hierarchy\n- Bulk operations on all devices under a parent\n- Exporting complete device trees\n- Displaying full hierarchy views\n\n**Path Parameters:**\n- device_id: UUID of the parent device\n\n**Query Parameters:**\n- include_descendants: bool (default: false)\n  - false: Return only direct children (1 level)\n  - true: Return all descendants (recursive, all levels)\n\n**Response Codes:**\n- 200: Success (returns empty array if no children)\n- 404: Device not found (DEVICE_NOT_FOUND)\n- 401: Invalid or missing API key\n\n**Response Format:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"dev_eui\": \"383936306c4b5880\",\n      \"device_name\": \"383936306C4B5880 24h\",\n      \"name\": \"Sub-Meter 001\",\n      \"device_type\": \"water_meter\",\n      \"status\": \"online\",\n      \"battery_level\": 3.65,\n      \"last_reading_at\": \"2025-01-01T12:00:00+00:00\",\n      \"is_active\": true,\n      \"end_user_id\": \"uuid\",\n      \"parent_device_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n      \"is_principal\": false,\n      \"hierarchy_level\": 1,\n      \"deleted_at\": null,\n      \"created_at\": \"2025-01-01T00:00:00+00:00\"\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Ordering:**\nResults are ordered by:\n1. hierarchy_level ASC (shallowest first)\n2. created_at DESC (newest first within each level)\n\nThis ordering ensures that when displaying descendants, parent devices\nappear before their children, making it easy to build tree views.\n\n**Empty Results:**\nIf the device has no children, this endpoint returns an empty array with\nstatus \"success\". This is not an error condition.\n\n**Soft Delete Behavior:**\n- Only non-deleted children are returned (WHERE deleted_at IS NULL)\n- Soft-deleted children are excluded from results\n- The parent device itself can be soft-deleted (query still works)\n\n**Multi-Tenant Security:**\n- Only children belonging to the authenticated reseller are returned\n- Cross-reseller device relationships are prevented automatically\n\n**Use Cases:**\n- Display device hierarchy in UI (tree view, expandable lists)\n- Calculate total consumption for a building or zone\n- Bulk operations on all devices under a parent\n- Export device hierarchies for reporting\n- Validate hierarchy structure before making changes\n\n**Performance:**\n- Direct children query: O(1) with proper indexes\n- Recursive descendants query: O(n) where n = total descendants\n- Both queries use indexes on (reseller_id, parent_device_id, hierarchy_level)\n\nRequirements: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 18.2","operationId":"get_device_children_api_v1_devices__device_id__children_get","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}},{"name":"include_descendants","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Descendants"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}/restore":{"put":{"tags":["devices"],"summary":"Restore Device","description":"Restore a soft-deleted device\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Device Restoration:**\nThis endpoint reverses a soft delete operation, restoring a device to active\nstatus. The restoration process:\n- Clears the deleted_at timestamp (sets to NULL)\n- Sets is_active to true (resumes billing)\n- Preserves all historical data and relationships\n- Checks for dev_eui collisions before restoring\n- Maintains parent-child relationships\n\n**Billing Impact:**\nRestored devices are immediately included in billing calculations. The\ndevice's is_active flag is set to true, causing it to count toward the\nmonthly device count starting from the restoration date.\n\n**Path Parameters:**\n- device_id: UUID of the device to restore\n\n**Response Codes:**\n- 200: Device restored successfully\n- 404: Device not found or not deleted (DEVICE_NOT_FOUND)\n- 400: dev_eui collision - another active device has same dev_eui (DEVICE_ALREADY_EXISTS)\n- 401: Invalid or missing API key\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"123e4567-e89b-12d3-a456-426614174000\",\n    \"dev_eui\": \"383936306c4b5880\",\n    \"device_name\": \"383936306C4B5880 24h\",\n    \"name\": \"Water Meter 001\",\n    \"device_type\": \"water_meter\",\n    \"status\": \"online\",\n    \"battery_level\": 3.65,\n    \"last_reading_at\": \"2025-01-01T12:00:00+00:00\",\n    \"is_active\": true,\n    \"end_user_id\": \"uuid\",\n    \"parent_device_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n    \"is_principal\": false,\n    \"hierarchy_level\": 1,\n    \"deleted_at\": null,\n    \"created_at\": \"2025-01-01T00:00:00+00:00\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Device restored successfully\"\n}\n```\n\n**dev_eui Collision Detection:**\nBefore restoring a device, the system checks if another active device\nalready has the same dev_eui. This prevents duplicate Device EUIs in the\nactive device inventory. If a collision is detected:\n- Restoration fails with 400 DEVICE_ALREADY_EXISTS\n- Error includes details about the existing device\n- Original device remains soft-deleted\n\n**Hierarchy Preservation:**\nDevice restoration maintains all hierarchy relationships:\n- parent_device_id remains unchanged\n- hierarchy_level remains unchanged\n- Child devices (if any) remain unaffected\n- Parent device can be soft-deleted (restoration still works)\n\n**Historical Data:**\nAll consumption data recorded while the device was deleted remains intact\nand associated with the device after restoration. This ensures complete\nhistorical accuracy.\n\n**Multi-Tenant Security:**\n- Only devices belonging to the authenticated reseller can be restored\n- Cross-reseller restorations are prevented automatically\n- dev_eui collision check is scoped to the reseller\n\n**Use Cases:**\n- Recover from accidental deletion\n- Reactivate temporarily decommissioned devices\n- Restore devices after maintenance period\n- Undo soft delete operations\n\n**Important Notes:**\n- Device must be soft-deleted (deleted_at IS NOT NULL) to be restored\n- Attempting to restore an active device returns 404\n- Restored devices appear immediately in GET /devices list\n- Restored devices can be assigned, updated, and have parent changed\n- ChirpStack data continues to be accepted for restored devices\n\n**Collision Scenario:**\nIf you soft-deleted device A (dev_eui: \"abc123\"), then registered a new\ndevice B with the same dev_eui \"abc123\", you cannot restore device A\nuntil device B is deleted or has its dev_eui changed. This prevents\nduplicate Device EUIs in the active inventory.\n\nRequirements: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7","operationId":"restore_device_api_v1_devices__device_id__restore_put","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}/ownership-history":{"get":{"tags":["devices"],"summary":"Get Device Ownership History","description":"Get ownership history for a device\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Ownership History Tracking:**\nThis endpoint returns a complete audit trail of device ownership changes,\nincluding when devices were assigned, reassigned, or unassigned. Each record\nincludes timestamps, user identifiers, and links to generated reports.\n\n**Path Parameters:**\n- device_id: Device UUID\n\n**Query Parameters:**\n- include_details: Include end user details (name, email) in response (default: false)\n\n**Response Codes:**\n- 200: Ownership history retrieved successfully\n- 404: Device not found (DEVICE_NOT_FOUND)\n- 401: Invalid or missing API key\n\n**Response Format (include_details=false):**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"device_id\": \"uuid\",\n      \"reseller_id\": \"uuid\",\n      \"previous_end_user_id\": \"uuid\",\n      \"new_end_user_id\": \"uuid\",\n      \"changed_at\": \"2025-11-11T10:00:00+00:00\",\n      \"changed_by\": \"api_key_name\",\n      \"report_generated\": true,\n      \"report_url\": \"reports/ownership/...\",\n      \"notes\": null,\n      \"created_at\": \"2025-11-11T10:00:00+00:00\"\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Response Format (include_details=true):**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"device_id\": \"uuid\",\n      \"reseller_id\": \"uuid\",\n      \"previous_end_user_id\": \"uuid\",\n      \"new_end_user_id\": \"uuid\",\n      \"changed_at\": \"2025-11-11T10:00:00+00:00\",\n      \"changed_by\": \"api_key_name\",\n      \"report_generated\": true,\n      \"report_url\": \"reports/ownership/...\",\n      \"notes\": null,\n      \"created_at\": \"2025-11-11T10:00:00+00:00\",\n      \"previous_end_user\": {\n        \"id\": \"uuid\",\n        \"name\": \"John Doe\",\n        \"email\": \"john@example.com\"\n      },\n      \"new_end_user\": {\n        \"id\": \"uuid\",\n        \"name\": \"Jane Smith\",\n        \"email\": \"jane@example.com\"\n      }\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Multi-Tenant Security:**\n- Only ownership history for devices belonging to the authenticated reseller\n- Cross-reseller history access is prevented automatically\n\n**Empty History:**\nIf a device has never been assigned, the response will contain an empty array.\n\n**Use Cases:**\n- Audit trail for compliance and dispute resolution\n- Track device lifecycle and ownership changes\n- Access final consumption reports for previous owners\n\nRequirements: 19.6, 19.8, 19.9","operationId":"get_device_ownership_history_api_v1_devices__device_id__ownership_history_get","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}},{"name":"include_details","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Details"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}/consumption":{"get":{"tags":["devices"],"summary":"Get Device Consumption","description":"Get water consumption history for a specific device\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Query Parameters:**\n- days: Number of days of history to retrieve (default: 30, max: 365)\n\n**Response Format:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"device_id\": \"uuid\",\n      \"timestamp\": \"2024-01-15T10:30:00Z\",\n      \"consumption_m3\": 0.1505,\n      \"battery_level\": 3.65\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Multi-Tenant Security:**\n- Only consumption data for devices belonging to the authenticated reseller\n- Cross-reseller data access is prevented automatically\n\n**Empty Data:**\nIf a device has no consumption data for the period, returns an empty array.\n\n**Use Cases:**\n- Display consumption charts in device details panel\n- Calculate daily/weekly/monthly consumption statistics\n- Identify consumption patterns and anomalies\n\nRequirements: 13.1, 13.2, 13.3, 13.4, 13.5 - Device consumption history access","operationId":"get_device_consumption_api_v1_devices__device_id__consumption_get","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}/consumption-per-day":{"get":{"tags":["devices"],"summary":"Get Device Consumption Per Day","description":"Get daily water consumption for a specific device.\n\nCalculates daily consumption as: last_reading_of_day - last_reading_of_previous_day.\nThis matches the cumulative meter reading model where each reading is a running total.\n\n**Authentication:** Requires X-API-Key header or JWT token\n\n**Parameters:**\n- device_id: Device UUID\n- days: Number of days (default 7, max 365)\n\n**Returns:** Array of daily data points with consumption, offline status,\nlast cumulative reading, and summary statistics.","operationId":"get_device_consumption_per_day_api_v1_devices__device_id__consumption_per_day_get","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":7,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/hierarchy-batch":{"patch":{"tags":["devices"],"summary":"Hierarchy Batch","description":"Batch-assign hierarchy levels to multiple devices in a single transaction.\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Request Body:**\n```json\n{\n  \"assignments\": [\n    { \"device_id\": \"uuid\", \"hierarchy_level\": 0 },\n    { \"device_id\": \"uuid\", \"hierarchy_level\": 1, \"parent_device_id\": \"uuid\" }\n  ]\n}\n```\n\n**Validation:**\n- All device_ids must belong to the authenticated reseller (400 VALIDATION_ERROR if not)\n- Child assignments (hierarchy_level > 0) must supply a parent_device_id that is a\n  principal device (hierarchy_level == 0) belonging to the same reseller\n\n**Response:**\n```json\n{ \"data\": { \"updated_count\": 2, \"devices\": [...] }, \"status\": \"success\" }\n```\n\n**Response Codes:**\n- 200: All assignments applied\n- 400: Validation error (unknown device_id or invalid parent)\n- 401: Invalid or missing authentication","operationId":"hierarchy_batch_api_v1_devices_hierarchy_batch_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HierarchyBatchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/bulk-assign":{"post":{"tags":["devices"],"summary":"Bulk Assign Devices","description":"Bulk assign multiple devices to an end user or unassign them.\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Request Body:**\n```json\n{\n  \"device_ids\": [\"uuid1\", \"uuid2\", \"uuid3\"],\n  \"end_user_id\": \"user-uuid\"  // null to unassign\n}\n```\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"succeeded\": [\n      {\"id\": \"uuid1\", \"dev_eui\": \"...\", ...}\n    ],\n    \"failed\": [\n      {\"device_id\": \"uuid2\", \"error\": \"Device not found\"}\n    ]\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Operation completed (check succeeded/failed arrays)\n- 401: Invalid or missing authentication","operationId":"bulk_assign_devices_api_v1_devices_bulk_assign_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkAssignRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/bulk-status":{"post":{"tags":["devices"],"summary":"Bulk Update Status","description":"Bulk update status for multiple devices.\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Request Body:**\n```json\n{\n  \"device_ids\": [\"uuid1\", \"uuid2\", \"uuid3\"],\n  \"status\": \"maintenance\"  // or \"auto\"\n}\n```\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"succeeded\": [\n      {\"id\": \"uuid1\", \"dev_eui\": \"...\", \"status\": \"maintenance\", ...}\n    ],\n    \"failed\": [\n      {\"device_id\": \"uuid2\", \"error\": \"Device not found\"}\n    ]\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Operation completed (check succeeded/failed arrays)\n- 401: Invalid or missing authentication","operationId":"bulk_update_status_api_v1_devices_bulk_status_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkStatusRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/bulk-delete":{"post":{"tags":["devices"],"summary":"Bulk Delete Devices","description":"Bulk soft-delete multiple devices.\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Request Body:**\n```json\n{\n  \"device_ids\": [\"uuid1\", \"uuid2\", \"uuid3\"]\n}\n```\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"succeeded\": [\n      {\"device_id\": \"uuid1\", \"deleted_at\": \"2025-01-01T12:00:00Z\"}\n    ],\n    \"failed\": [\n      {\"device_id\": \"uuid2\", \"error\": \"Device not found\"}\n    ]\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Operation completed (check succeeded/failed arrays)\n- 401: Invalid or missing authentication","operationId":"bulk_delete_devices_api_v1_devices_bulk_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkDeleteRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}/assign-customer":{"put":{"tags":["devices"],"summary":"Assign Device To Customer","description":"Assign a device to a customer (community).\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Customer Assignment:**\nThis endpoint assigns a device to a customer by creating or updating an end user\nassociation. When a device is assigned to a customer, it becomes visible in that\ncustomer's dashboard and can be further assigned to specific end users within\nthat customer.\n\n**How Customer Assignment Works:**\nDevices are associated with customers through end users. When you assign a device\nto a customer:\n1. If the device has no end_user_id, a default end user for that customer is used\n   (or the device's customer_id is set directly if the schema supports it)\n2. The device becomes part of that customer's device fleet\n3. The device appears in filtered views for that customer\n\n**Path Parameters:**\n- device_id: Device UUID\n\n**Request Body:**\n```json\n{\n  \"customer_id\": \"123e4567-e89b-12d3-a456-426614174000\"\n}\n```\n\n**Response Codes:**\n- 200: Device assigned to customer successfully\n- 400: Invalid request (validation error)\n- 404: Device not found (DEVICE_NOT_FOUND)\n- 404: Customer not found (CUSTOMER_NOT_FOUND)\n- 401: Invalid or missing authentication\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"dev_eui\": \"383936306c4b5880\",\n    \"customer_id\": \"uuid\",\n    \"customer_name\": \"Building A\",\n    ...\n  },\n  \"status\": \"success\",\n  \"message\": \"Device assigned to Building A\"\n}\n```\n\n**Multi-Tenant Security:**\n- Device must belong to the authenticated reseller\n- Customer must belong to the authenticated reseller\n- Cross-reseller assignments are prevented automatically\n\n**Use Cases:**\n- Assign unassociated devices to the correct customer\n- Fix devices that couldn't be auto-associated via application_id matching\n- Move devices between customers\n\nRequirements: 4.1-4.8 (Inline Customer Assignment)","operationId":"assign_device_to_customer_api_v1_devices__device_id__assign_customer_put","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignCustomerRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/bulk-assign-customer":{"post":{"tags":["devices"],"summary":"Bulk Assign Devices To Customer","description":"Bulk assign multiple devices to a customer (community).\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Atomic Operation:**\nThis endpoint performs an atomic bulk assignment. Either all devices are\nassigned successfully, or none are assigned (all-or-nothing semantics).\nThis ensures data consistency when assigning multiple devices.\n\n**Request Body:**\n```json\n{\n  \"device_ids\": [\"uuid1\", \"uuid2\", \"uuid3\"],\n  \"customer_id\": \"customer-uuid\"\n}\n```\n\n**Response Codes:**\n- 200: All devices assigned successfully\n- 400: Validation error (invalid device_ids or customer_id)\n- 404: Customer not found (CUSTOMER_NOT_FOUND)\n- 400: One or more devices not found or don't belong to reseller (DEVICES_NOT_FOUND)\n- 401: Invalid or missing authentication\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"assigned_count\": 3\n  },\n  \"status\": \"success\",\n  \"message\": \"3 devices assigned to Building A\"\n}\n```\n\n**Multi-Tenant Security:**\n- All devices must belong to the authenticated reseller\n- Customer must belong to the authenticated reseller\n- Cross-reseller assignments are prevented automatically\n\n**Atomicity Guarantee:**\nIf any device in the list cannot be assigned (not found, wrong reseller, etc.),\nthe entire operation fails and no devices are modified. This prevents partial\nupdates that could leave the system in an inconsistent state.\n\n**Use Cases:**\n- Bulk assign newly discovered devices to a customer\n- Move multiple devices between customers\n- Initial setup when onboarding a new customer with many devices\n\nRequirements: 6.3-6.5 (Bulk Actions)","operationId":"bulk_assign_devices_to_customer_api_v1_devices_bulk_assign_customer_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkAssignCustomerRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}/assign-user":{"put":{"tags":["devices"],"summary":"Assign Device To User","description":"Assign a device to an end user or remove the assignment.\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**User Assignment:**\nThis endpoint assigns a device to a specific end user. If the device has a\ncustomer association (via end_user.customer_id), the target user must belong\nto the same customer to maintain data integrity.\n\n**Path Parameters:**\n- device_id: Device UUID\n\n**Request Body:**\n```json\n{\n  \"end_user_id\": \"123e4567-e89b-12d3-a456-426614174000\"\n}\n```\n\nTo remove the user assignment:\n```json\n{\n  \"end_user_id\": null\n}\n```\n\n**Customer Preservation on User Removal:**\nWhen removing a user from a device, the customer association is automatically\npreserved. If the device's customer was derived from the user (via end_user.customer_id),\nthe customer_id is copied directly to the device before removing the user.\nThis ensures the device remains associated with the customer even without a user.\n\n**Response Codes:**\n- 200: Device assigned to user successfully\n- 400: User doesn't belong to device's customer (USER_CUSTOMER_MISMATCH)\n- 404: Device not found (DEVICE_NOT_FOUND)\n- 404: User not found (USER_NOT_FOUND)\n- 401: Invalid or missing authentication\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"dev_eui\": \"383936306c4b5880\",\n    \"end_user_id\": \"uuid\",\n    \"end_user_name\": \"Juan García\",\n    ...\n  },\n  \"status\": \"success\",\n  \"message\": \"Device assigned to Juan García\"\n}\n```\n\n**Multi-Tenant Security:**\n- Device must belong to the authenticated reseller\n- User must belong to the authenticated reseller\n- If device has a customer, user must belong to that customer\n\n**Customer Context Filtering:**\n- If device has customer_id (via end_user), only users from that customer are valid\n- This prevents accidental cross-customer user assignments\n\nRequirements: 5b.1-5b.22 (Inline User Assignment)","operationId":"assign_device_to_user_api_v1_devices__device_id__assign_user_put","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignUserRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/bulk-assign-user":{"post":{"tags":["devices"],"summary":"Bulk Assign Devices To User","description":"Bulk assign multiple devices to an end user or remove assignments.\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Atomic Operation:**\nThis endpoint performs an atomic bulk assignment. Either all devices are\nassigned successfully, or none are assigned (all-or-nothing semantics).\n\n**Customer Context Warning:**\nIf the selected devices belong to different customers, a warning is returned\nin the response but the operation still proceeds. This helps resellers\nunderstand potential data organization issues.\n\n**Customer Preservation on User Removal:**\nWhen removing users from devices, the customer association is automatically\npreserved for each device. If a device's customer was derived from the user\n(via end_user.customer_id), the customer_id is copied directly to the device\nbefore removing the user. This ensures devices remain associated with their\ncustomers even without users.\n\n**Request Body:**\n```json\n{\n  \"device_ids\": [\"uuid1\", \"uuid2\", \"uuid3\"],\n  \"end_user_id\": \"user-uuid\"\n}\n```\n\nTo remove user assignments:\n```json\n{\n  \"device_ids\": [\"uuid1\", \"uuid2\", \"uuid3\"],\n  \"end_user_id\": null\n}\n```\n\n**Response Codes:**\n- 200: All devices assigned successfully\n- 400: Validation error (invalid device_ids or end_user_id)\n- 404: User not found (USER_NOT_FOUND)\n- 400: One or more devices not found or don't belong to reseller (DEVICES_NOT_FOUND)\n- 401: Invalid or missing authentication\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"assigned_count\": 3,\n    \"warning\": \"Selected devices belong to different customers\"\n  },\n  \"status\": \"success\",\n  \"message\": \"3 devices assigned to Juan García\"\n}\n```\n\n**Multi-Tenant Security:**\n- All devices must belong to the authenticated reseller\n- User must belong to the authenticated reseller\n\n**Use Cases:**\n- Assign multiple devices to a single end user (e.g., same apartment)\n- Remove user assignments from multiple devices\n- Bulk reassignment when end user changes\n\nRequirements: 6b.1-6b.8 (Bulk User Assignment)","operationId":"bulk_assign_devices_to_user_api_v1_devices_bulk_assign_user_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkAssignUserRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}/transfer-ownership":{"post":{"tags":["devices"],"summary":"Transfer Device Ownership","description":"Transfer device ownership from current user to a new user.\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Ownership Transfer Process:**\nThis is a formal ownership transfer that:\n1. Validates the device has a current owner\n2. Gets the last known reading as baseline for the new owner\n3. Generates a final consumption report for the previous owner\n4. Records the transfer in ownership history\n5. Updates the device assignment to the new user\n\n**Important:** IoT devices typically report every 6 hours. The transfer uses\nthe last known reading as the baseline, not a real-time reading. This ensures\naccurate consumption tracking without waiting for the next device report.\n\n**Path Parameters:**\n- device_id: Device UUID\n\n**Request Body:**\n```json\n{\n  \"new_user_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n  \"notes\": \"Tenant moved out. New tenant moved in.\"\n}\n```\n\n**Response Codes:**\n- 200: Ownership transferred successfully\n- 400: Device has no current owner (DEVICE_NOT_ASSIGNED)\n- 400: Cannot transfer to same user (SAME_USER_TRANSFER)\n- 400: User doesn't belong to device's customer (USER_CUSTOMER_MISMATCH)\n- 404: Device not found (DEVICE_NOT_FOUND)\n- 404: New user not found (USER_NOT_FOUND)\n- 401: Invalid or missing authentication\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"device_id\": \"uuid\",\n    \"previous_user_id\": \"uuid\",\n    \"previous_user_name\": \"John Doe\",\n    \"new_user_id\": \"uuid\",\n    \"new_user_name\": \"Jane Smith\",\n    \"baseline_reading_m3\": 125.450,\n    \"baseline_timestamp\": \"2026-01-15T10:30:00+00:00\",\n    \"transfer_timestamp\": \"2026-01-15T14:30:00+00:00\",\n    \"report_url\": \"reports/ownership/...\",\n    \"ownership_history_id\": \"uuid\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Device ownership transferred from John Doe to Jane Smith\"\n}\n```\n\n**Multi-Tenant Security:**\n- Device must belong to the authenticated reseller\n- Both previous and new users must belong to the authenticated reseller\n- If device has a customer, new user must belong to that customer\n\n**Report Generation:**\nA final consumption report is generated for the previous owner covering\ntheir ownership period. The report is stored in Supabase Storage and the\nURL is returned in the response. Report generation is non-blocking - if\nit fails, the transfer still completes successfully.\n\nRequirements: 19.1-19.9 (Device Ownership Transfer)","operationId":"transfer_device_ownership_api_v1_devices__device_id__transfer_ownership_post","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferOwnershipRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/devices/{device_id}/ownership-report/{history_id}/download":{"get":{"tags":["devices"],"summary":"Download Ownership Report","description":"Download ownership change report CSV via signed URL redirect.\n\nGenerates a temporary signed URL for the report stored in Supabase Storage\nand redirects the browser to it for download.\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Path Parameters:**\n- device_id: Device UUID\n- history_id: Ownership history record UUID\n\n**Response:**\n- 302: Redirect to signed download URL\n- 404: Report not found or not generated","operationId":"download_ownership_report_api_v1_devices__device_id__ownership_report__history_id__download_get","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}},{"name":"history_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"History Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reseller/billing":{"get":{"tags":["reseller"],"summary":"Get Billing","description":"Calculate current billing for authenticated reseller based on subscription tier\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Billing Formula by Tier:**\n- Starter: $29 + max(0, active_devices - 25) × $0.50\n- Growth: $79 + max(0, active_devices - 100) × $0.50\n- Founding: $19 + max(0, active_devices - 10) × $0.25 (legacy)\n\n**Active Devices:**\nOnly devices with `is_active=true` count toward billing. This allows\nresellers to exclude devices during maintenance or decommissioning without\ndeleting them.\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"subscription_tier\": \"starter\",\n    \"tier_name\": \"Starter\",\n    \"base_cost\": 29.00,\n    \"included_devices\": 10,\n    \"overage_rate\": 0.45,\n    \"active_device_count\": 50,\n    \"overage_devices\": 40,\n    \"overage_cost\": 18.00,\n    \"monthly_cost\": 47.00,\n    \"is_legacy_tier\": false,\n    \"billing_period\": {\n      \"start\": \"2025-01-01T00:00:00Z\",\n      \"end\": \"2025-01-31T23:59:59Z\"\n    }\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success\n- 401: Invalid or missing API key\n- 404: Reseller not found\n\n**Calculation Notes:**\n- Billing is calculated on-demand (no daily snapshots for MVP)\n- Tier-specific base cost and included devices\n- Inactive devices (is_active=false) do not count toward billing\n- Unknown tiers fallback to Starter tier\n\n**Use Cases:**\n- Display current billing in reseller admin dashboard\n- Calculate projected costs before activating devices\n- Verify billing before monthly invoice\n- Show tier-specific pricing details","operationId":"get_billing_api_v1_reseller_billing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}}},"/api/v1/reseller/subscription":{"put":{"tags":["reseller"],"summary":"Update Subscription Tier","description":"Update subscription tier (self-service tier changes)\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Allowed Tier Changes:**\n- Starter ↔ Growth (bidirectional)\n- Founding tier cannot be selected or changed via API (manual only)\n\n**Request Body:**\n```json\n{\n  \"tier\": \"growth\"\n}\n```\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"subscription_tier\": \"growth\",\n    \"tier_name\": \"Growth\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Subscription tier updated to Growth\"\n}\n```\n\n**Response Codes:**\n- 200: Tier updated successfully\n- 400: Invalid tier change (same tier, or founding tier involved)\n- 401: Invalid or missing API key\n- 403: Cannot change to/from founding tier\n\n**Error Codes:**\n- SAME_TIER: Requested tier is already current tier\n- FOUNDING_TIER_RESTRICTED: Cannot change to/from founding tier via self-service\n\n**Billing Impact:**\nBilling is recalculated immediately after tier change based on current\nactive device count and new tier pricing.\n\n**Use Cases:**\n- Upgrade from Starter to Growth when scaling beyond 100 devices\n- Downgrade from Growth to Starter to reduce costs\n- Self-service tier management without admin intervention","operationId":"update_subscription_tier_api_v1_reseller_subscription_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionTierUpdateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reseller/settings":{"get":{"tags":["reseller"],"summary":"Get Settings","description":"Get white-label settings for authenticated reseller\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"name\": \"AquaLinks\",\n    \"subdomain\": \"aqualinks\",\n    \"tenant_name\": \"aqualinks-tenant\",\n    \"logo_url\": \"https://storage.supabase.co/...\",\n    \"primary_color\": \"#1E40AF\",\n    \"terminology\": {\n      \"reseller\": \"Organization\",\n      \"customer\": \"Community\",\n      \"end_user\": \"Resident\",\n      \"device\": \"Water Meter\"\n    }\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success\n- 401: Invalid or missing API key","operationId":"get_settings_api_v1_reseller_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}},"put":{"tags":["reseller"],"summary":"Update Settings","description":"Update white-label settings for authenticated reseller\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Request Body:**\n```json\n{\n  \"name\": \"My Organization\",\n  \"logo_url\": \"https://storage.supabase.co/...\",\n  \"primary_color\": \"#1E40AF\",\n  \"tenant_name\": \"my-org-tenant\"\n}\n```\n\n**Optional Fields:**\n- name: Organization name (1-100 characters). If omitted, current value is preserved\n- logo_url: If omitted, current value is preserved\n- primary_color: If omitted, current value is preserved\n- tenant_name: Default tenant name for webhook data ingestion (used when no customer exists)\n\n**Response Codes:**\n- 200: Settings updated successfully\n- 401: Invalid or missing API key\n- 400: Invalid color format or empty name","operationId":"update_settings_api_v1_reseller_settings_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResellerSettingsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reseller/terminology":{"put":{"tags":["reseller"],"summary":"Update Terminology","description":"Update UI terminology customization (Post-MVP feature)\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Terminology Customization:**\nAllows resellers to customize UI labels to match their vertical:\n- Residential: Community → Resident → Water Meter\n- Commercial: Building → Tenant → Meter\n- Municipal: District → Subscriber → Smart Meter\n- Industrial: Site → User → Sensor\n\n**Request Body:**\n```json\n{\n  \"reseller\": \"Organization\",\n  \"customer\": \"Building\",\n  \"end_user\": \"Tenant\",\n  \"device\": \"Meter\"\n}\n```\n\n**Response Codes:**\n- 200: Terminology updated successfully\n- 401: Invalid or missing API key","operationId":"update_terminology_api_v1_reseller_terminology_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResellerTerminologyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reseller/api-key":{"get":{"tags":["reseller"],"summary":"Get Api Key","description":"Get the reseller's current API key (masked)\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\nSince API keys are stored as bcrypt hashes, the plaintext key cannot be\nreversed. This endpoint always returns a masked version. Use the\nregenerate endpoint to get a new plaintext key.\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"api_key\": \"rsk_****...\",\n    \"masked\": true\n  },\n  \"status\": \"success\",\n  \"message\": \"API key is masked. Use regenerate endpoint to get a new plaintext key.\"\n}\n```\n\n**Response Codes:**\n- 200: Masked API key returned\n- 401: Invalid or missing authentication\n- 404: Reseller not found\n- 500: Failed to retrieve API key","operationId":"get_api_key_api_v1_reseller_api_key_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}}},"/api/v1/reseller/regenerate-api-key":{"post":{"tags":["reseller"],"summary":"Regenerate Api Key","description":"Regenerate API key for webhook authentication\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**WARNING:** This action is irreversible. The current API key will be\ninvalidated immediately and all webhook integrations using the old key\nwill stop working.\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"api_key\": \"rsk_new_generated_key_here\"\n  },\n  \"status\": \"success\",\n  \"message\": \"API key regenerated. Copy it now — it won't be shown again.\"\n}\n```\n\n**Response Codes:**\n- 200: API key regenerated successfully\n- 401: Invalid or missing authentication\n- 500: Failed to regenerate API key\n\n**Use Cases:**\n- Rotate API key for security compliance\n- Invalidate compromised API key\n- Reset API key after team member leaves\n\n**Post-Regeneration Steps:**\n1. Copy the new API key from the response\n2. Update your ChirpStack/TTN webhook configuration\n3. Verify webhook data is flowing correctly","operationId":"regenerate_api_key_api_v1_reseller_regenerate_api_key_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}}},"/api/v1/reseller/set-api-key":{"post":{"tags":["reseller"],"summary":"Set Custom Api Key","description":"Set a custom API key provided by the reseller.\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\nThe reseller provides their own key value. It is hashed with bcrypt\nbefore storage — the plaintext is never persisted.\n\n**Validation:**\n- Minimum 16 characters\n- Maximum 128 characters\n\n**Response Codes:**\n- 200: API key set successfully\n- 400: Invalid key (too short / too long)\n- 401: Invalid or missing authentication\n- 500: Failed to set API key","operationId":"set_custom_api_key_api_v1_reseller_set_api_key_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}}},"/api/v1/reseller/subdomain":{"put":{"tags":["reseller"],"summary":"Update Subdomain","description":"Update subdomain for authenticated reseller\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Request Body:**\n```json\n{ \"subdomain\": \"my-company\" }\n```\n\n**Validation Rules:**\n- Lowercase alphanumeric characters and hyphens only\n- Between 3 and 63 characters\n- Cannot start or end with a hyphen\n\n**Response Codes:**\n- 200: Subdomain updated successfully\n- 400: Invalid subdomain format (handled by Pydantic validation)\n- 401: Invalid or missing authentication\n- 409: Subdomain already taken by another reseller\n- 500: Failed to update subdomain","operationId":"update_subdomain_api_v1_reseller_subdomain_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubdomainUpdateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reseller/alarm-webhook":{"get":{"tags":["reseller"],"summary":"Get Alarm Webhook Config","description":"Get current webhook configuration for alarm notifications\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"webhook_url\": \"https://example.com/webhooks/alarms\",\n    \"webhook_enabled\": true\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success\n- 401: Invalid or missing API key\n\n**Use Cases:**\n- Display current webhook configuration in admin dashboard\n- Verify webhook settings before testing\n- Check if webhook notifications are enabled","operationId":"get_alarm_webhook_config_api_v1_reseller_alarm_webhook_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}},"put":{"tags":["reseller"],"summary":"Configure Alarm Webhook","description":"Configure webhook URL for alarm notifications\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Webhook Notifications:**\nWhen enabled, the system will POST alarm details to this URL when daily\nconsumption thresholds are exceeded. This allows resellers to integrate\nalarms into their own systems.\n\n**Request Body:**\n```json\n{\n  \"webhook_url\": \"https://example.com/webhooks/alarms\",\n  \"webhook_enabled\": true\n}\n```\n\n**Webhook Payload Format:**\nWhen an alarm is triggered, the system sends:\n```json\n{\n  \"alarm_id\": \"uuid\",\n  \"device_id\": \"uuid\",\n  \"dev_eui\": \"383936306c4b5880\",\n  \"device_name\": \"Water Meter 001\",\n  \"alarm_date\": \"2024-01-15\",\n  \"daily_consumption_liters\": 250.5,\n  \"threshold_liters\": 150.0,\n  \"exceeded_by_liters\": 100.5,\n  \"timestamp\": \"2024-01-16T00:30:00Z\"\n}\n```\n\n**Webhook Requirements:**\n- Must be HTTPS (HTTP not allowed for security)\n- Must respond with 2xx status code within 10 seconds\n- Failed deliveries are retried up to 3 times with 60-second delays\n\n**Response Codes:**\n- 200: Webhook configured successfully\n- 400: Invalid webhook URL (must be HTTPS)\n- 401: Invalid or missing API key\n\n**Error Codes:**\n- INVALID_WEBHOOK_URL: URL is not valid HTTPS format","operationId":"configure_alarm_webhook_api_v1_reseller_alarm_webhook_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookConfig"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reseller/trigger-report-generation":{"post":{"tags":["reseller"],"summary":"Trigger Report Generation","description":"Manually trigger the report generation job (admin only)\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Purpose:**\nAllows reseller admins to manually trigger the report generation job\nwithout waiting for the scheduled run at 02:00 UTC. Useful for:\n- Testing report generation\n- Generating reports on-demand\n- Verifying storage fix\n\n**Behavior:**\n- Runs the same job as the scheduled task\n- Processes all eligible users (enable_reports=true)\n- Generates reports for the previous month\n- Uploads to Supabase Storage\n- Returns statistics on success/failure\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"total_users\": 10,\n    \"successful_reports\": 8,\n    \"failed_reports\": 2,\n    \"duration_seconds\": 5.2,\n    \"success_rate\": 80.0\n  },\n  \"status\": \"success\",\n  \"message\": \"Report generation completed\"\n}\n```\n\n**Error Responses:**\n- 401: Invalid API key\n- 500: Job execution failed\n\n**Example:**\n```bash\ncurl -X POST https://api.datakubo.com/api/v1/reseller/trigger-report-generation       -H \"X-API-Key: reseller_abc123\"\n```","operationId":"trigger_report_generation_api_v1_reseller_trigger_report_generation_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}}},"/api/v1/customers":{"get":{"tags":["customers"],"summary":"List Customers","description":"List all customers for the authenticated reseller\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Multi-Tenant Filtering:**\nThis endpoint automatically filters customers by the authenticated reseller.\nEach reseller can only see their own customers.\n\n**Response Format:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"reseller_id\": \"uuid\",\n      \"name\": \"Riverside HOA\",\n      \"tenant_name\": \"riverside-hoa\",\n      \"admin_email\": \"admin@riverside-hoa.com\",\n      \"status\": \"active\",\n      \"device_count\": 25,\n      \"end_user_count\": 18,\n      \"created_at\": \"2025-01-01T00:00:00+00:00\"\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success (returns empty array if no customers exist)\n- 401: Invalid or missing API key\n\n**Customer Mode Detection:**\nIf this endpoint returns an empty array, the UI operates in Direct Mode.\nIf it returns customers, the UI switches to Customer Mode.","operationId":"list_customers_api_v1_customers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}},"post":{"tags":["customers"],"summary":"Create Customer","description":"Create a new customer for the authenticated reseller\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Customer Mode:**\nCustomers represent the reseller's customers (HOAs, co-ops, buildings, facilities).\nCreating a customer automatically enables Customer Mode in the UI.\n\n**Request Body:**\n```json\n{\n  \"name\": \"Riverside HOA\",\n  \"tenant_name\": \"riverside-hoa\",\n  \"admin_email\": \"admin@riverside-hoa.com\"\n}\n```\n\n**Response Codes:**\n- 200: Customer created successfully\n- 400: Customer with tenant_name already exists\n- 401: Invalid or missing API key\n- 422: Invalid payload\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"reseller_id\": \"uuid\",\n    \"name\": \"Riverside HOA\",\n    \"tenant_name\": \"riverside-hoa\",\n    \"admin_email\": \"admin@riverside-hoa.com\",\n    \"status\": \"active\",\n    \"created_at\": \"2025-01-01T00:00:00+00:00\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Customer created successfully\"\n}\n```\n\n**ChirpStack Integration:**\nThe tenant_name field maps to ChirpStack's deviceInfo.tenantName for automatic\ndevice-to-customer association during webhook ingestion.","operationId":"create_customer_api_v1_customers_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerCreateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/customers/{customer_id}":{"get":{"tags":["customers"],"summary":"Get Customer","description":"Get customer details by ID\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"reseller_id\": \"uuid\",\n    \"name\": \"Riverside HOA\",\n    \"tenant_name\": \"riverside-hoa\",\n    \"admin_email\": \"admin@riverside-hoa.com\",\n    \"status\": \"active\",\n    \"device_count\": 25,\n    \"end_user_count\": 18,\n    \"created_at\": \"2025-01-01T00:00:00+00:00\"\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Customer found\n- 404: Customer not found\n- 401: Invalid or missing API key","operationId":"get_customer_api_v1_customers__customer_id__get","parameters":[{"name":"customer_id","in":"path","required":true,"schema":{"type":"string","title":"Customer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["customers"],"summary":"Update Customer","description":"Update customer details\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Request Body:**\n```json\n{\n  \"name\": \"Riverside HOA Updated\",\n  \"admin_email\": \"newemail@riverside-hoa.com\",\n  \"status\": \"inactive\"\n}\n```\n\n**Optional Fields:**\nAll fields are optional. Only provided fields will be updated.\n\n**Response Codes:**\n- 200: Customer updated successfully\n- 404: Customer not found\n- 401: Invalid or missing API key","operationId":"update_customer_api_v1_customers__customer_id__put","parameters":[{"name":"customer_id","in":"path","required":true,"schema":{"type":"string","title":"Customer Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["customers"],"summary":"Delete Customer","description":"Delete a customer\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Warning:**\nDeleting a customer will also unassign all end users from that customer\n(sets customer_id to NULL). Devices remain assigned to end users.\n\n**Response Codes:**\n- 200: Customer deleted successfully\n- 404: Customer not found\n- 401: Invalid or missing API key","operationId":"delete_customer_api_v1_customers__customer_id__delete","parameters":[{"name":"customer_id","in":"path","required":true,"schema":{"type":"string","title":"Customer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/customers/{customer_id}/register-unregistered-devices":{"post":{"tags":["customers"],"summary":"Bulk Register Unregistered Devices","description":"Bulk register all pending unregistered devices for a customer\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Use Case:**\nWhen a reseller deploys multiple devices for a customer, ChirpStack may receive\ndata from devices before they are registered in DataKubo. These devices are\ntracked in the unregistered_devices table with status='pending'. This endpoint\nallows the reseller to register all pending devices for a customer at once.\n\n**Requirements:** 20.1-20.13\n\n**Request Body (Optional):**\n```json\n{\n  \"device_type\": \"water_meter\",\n  \"default_end_user_id\": \"123e4567-e89b-12d3-a456-426614174000\"\n}\n```\n\n**Optional Fields:**\n- device_type: Default device type for all devices (default: \"water_meter\")\n- default_end_user_id: Optional end user to assign all devices to\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"registered_count\": 5,\n    \"devices\": [\n      {\n        \"id\": \"uuid\",\n        \"dev_eui\": \"383936306c4b5880\",\n        \"name\": \"383936306C4B5880 24h\",\n        \"device_type\": \"water_meter\",\n        \"status\": \"offline\",\n        \"is_active\": true,\n        \"end_user_id\": \"uuid\",\n        ...\n      }\n    ]\n  },\n  \"status\": \"success\",\n  \"message\": \"Successfully registered 5 pending devices for customer\"\n}\n```\n\n**Response Codes:**\n- 200: Success (registered_count may be 0 if no pending devices)\n- 404: Customer not found (CUSTOMER_NOT_FOUND)\n- 404: End user not found (USER_NOT_FOUND) if default_end_user_id provided\n- 401: Invalid or missing API key\n- 500: Bulk registration failed (BULK_REGISTRATION_FAILED)\n\n**Transaction Atomicity:**\nThe entire operation is wrapped in a database transaction. If any device\nregistration fails, all changes are rolled back (all-or-nothing).\n\n**Status Filtering:**\nOnly devices with status='pending' are registered. Devices with status='registered'\nor 'ignored' are excluded automatically.\n\n**Multi-Tenant Security:**\nOnly unregistered devices belonging to the authenticated reseller are processed.","operationId":"bulk_register_unregistered_devices_api_v1_customers__customer_id__register_unregistered_devices_post","parameters":[{"name":"customer_id","in":"path","required":true,"schema":{"type":"string","title":"Customer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/end-users":{"post":{"tags":["end_users"],"summary":"Create End User","description":"Create a new end user for the authenticated reseller\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Operating Modes:**\n- Direct Mode: customer_id is NULL (end user belongs directly to reseller)\n- Customer Mode: customer_id is provided (end user belongs to a customer)\n\n**Request Body:**\n```json\n{\n  \"email\": \"resident@example.com\",\n  \"name\": \"John Doe\",\n  \"customer_id\": \"uuid\"\n}\n```\n\n**Response Codes:**\n- 200: End user created successfully\n- 400: End user with email already exists\n- 404: Customer not found (if customer_id provided)\n- 401: Invalid or missing API key\n- 422: Invalid payload\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"reseller_id\": \"uuid\",\n    \"customer_id\": \"uuid\",\n    \"email\": \"resident@example.com\",\n    \"name\": \"John Doe\",\n    \"created_at\": \"2025-01-01T00:00:00+00:00\"\n  },\n  \"status\": \"success\",\n  \"message\": \"End user created successfully\"\n}\n```","operationId":"create_end_user_api_v1_end_users_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EndUserCreateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["end_users"],"summary":"List End Users","description":"List all end users for the authenticated reseller\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Query Parameters:**\n- customer_id (optional): Filter by customer ID\n- search (optional): Search by name or email (case-insensitive)\n- limit (optional): Limit number of results (default: no limit)\n- recent (optional): If true, return users grouped by recent/all for dropdown UI\n\n**Multi-Tenant Filtering:**\nThis endpoint automatically filters end users by the authenticated reseller.\n\n**Response Format (standard):**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"reseller_id\": \"uuid\",\n      \"customer_id\": \"uuid\",\n      \"email\": \"resident@example.com\",\n      \"name\": \"John Doe\",\n      \"device_count\": 2,\n      \"created_at\": \"2025-01-01T00:00:00+00:00\"\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Response Format (with recent=true):**\n```json\n{\n  \"data\": {\n    \"recent\": [\n      {\"id\": \"uuid\", \"name\": \"Juan García\", \"email\": \"juan@example.com\"},\n      {\"id\": \"uuid\", \"name\": \"María López\", \"email\": \"maria@example.com\"}\n    ],\n    \"all\": [\n      {\"id\": \"uuid\", \"name\": \"Ana Martínez\", \"email\": \"ana@example.com\"},\n      ...\n    ]\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success (returns empty array if no end users exist)\n- 401: Invalid or missing API key\n\nRequirements: 5b.9-5b.11 (User dropdown with recent section)","operationId":"list_end_users_api_v1_end_users_get","parameters":[{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Id"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}},{"name":"recent","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Recent"}},{"name":"individual","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Individual"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/end-users/{end_user_id}":{"get":{"tags":["end_users"],"summary":"Get End User","description":"Get end user details by ID\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"reseller_id\": \"uuid\",\n    \"customer_id\": \"uuid\",\n    \"email\": \"resident@example.com\",\n    \"name\": \"John Doe\",\n    \"device_count\": 2,\n    \"created_at\": \"2025-01-01T00:00:00+00:00\"\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: End user found\n- 404: End user not found\n- 401: Invalid or missing API key","operationId":"get_end_user_api_v1_end_users__end_user_id__get","parameters":[{"name":"end_user_id","in":"path","required":true,"schema":{"type":"string","title":"End User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["end_users"],"summary":"Update End User","description":"Update end user details\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Request Body:**\n```json\n{\n  \"name\": \"John Doe Updated\",\n  \"email\": \"newemail@example.com\",\n  \"customer_id\": \"uuid\"\n}\n```\n\n**Optional Fields:**\nAll fields are optional. Only provided fields will be updated.\n\n**Response Codes:**\n- 200: End user updated successfully\n- 404: End user not found\n- 404: Customer not found (if customer_id provided)\n- 401: Invalid or missing API key","operationId":"update_end_user_api_v1_end_users__end_user_id__put","parameters":[{"name":"end_user_id","in":"path","required":true,"schema":{"type":"string","title":"End User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EndUserUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["end_users"],"summary":"Delete End User","description":"Delete an end user\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Warning:**\nDeleting an end user will also unassign all devices from that user\n(sets end_user_id to NULL on devices). Devices are not deleted.\n\n**Response Codes:**\n- 200: End user deleted successfully\n- 404: End user not found\n- 401: Invalid or missing API key","operationId":"delete_end_user_api_v1_end_users__end_user_id__delete","parameters":[{"name":"end_user_id","in":"path","required":true,"schema":{"type":"string","title":"End User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reports/generate":{"post":{"tags":["reports"],"summary":"Generate Report","description":"Generate a customer-level report on-demand with date range presets.\n\nGenerates a CSV report containing all end users and their devices for the\nspecified customer and date range. The report is generated synchronously\nwith a 30-second timeout and stored in Supabase Storage.\n\nAuthentication: X-API-Key header OR JWT token required (reseller-level)\n\nDate Range Presets:\n- last_month: First and last day of previous month\n- last_3_months: First day 3 months ago to today\n- since_last_report: Day after last report's period_end to today\n- custom: User-specified start_date and end_date\n\nRequest Body:\n- customer_id: Customer UUID (required for reseller)\n- preset: Date range preset (default: custom)\n- start_date: Start date (required if preset=custom)\n- end_date: End date (required if preset=custom)\n\nResponse:\n- 200: Success with report metadata\n- 400: Invalid date range or missing previous report\n- 403: Customer doesn't belong to reseller\n- 504: Generation timeout (>30 seconds)\n- 500: Generation failed\n\nExample:\nPOST /api/v1/reports/generate\nX-API-Key: <reseller_api_key>\n\n{\n    \"customer_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n    \"preset\": \"last_month\"\n}\n\nOR with custom dates:\n\n{\n    \"customer_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n    \"preset\": \"custom\",\n    \"start_date\": \"2024-01-01\",\n    \"end_date\": \"2024-01-31\"\n}\n\nResponse:\n{\n    \"data\": {\n        \"id\": \"uuid\",\n        \"reseller_id\": \"uuid\",\n        \"customer_id\": \"uuid\",\n        \"period_start\": \"2024-01-01\",\n        \"period_end\": \"2024-01-31\",\n        \"csv_path\": \"reseller_id/customer_id/2024-01-01_to_2024-01-31.csv\",\n        \"csv_size_bytes\": 15234,\n        \"generated_by\": \"user@example.com\",\n        \"generated_at\": \"2024-02-01T10:30:00Z\"\n    },\n    \"status\": \"success\"\n}\n\nRequirements: 1.1, 1.2, 1.14, 2.1-2.8, 4.1-4.8, 5.3-5.5, 7.1-7.5","operationId":"generate_report_api_v1_reports_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateReportRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reports":{"get":{"tags":["reports"],"summary":"List Reports","description":"List reports for the authenticated reseller.\n\nReturns reports sorted by generation date (most recent first).\nOptionally filter by customer_id to show reports for a specific customer.\nCustomer admins (role='customer') automatically see only their own customer's reports.\n\nAccess Control:\n- Reseller: Can list all reports for their customers\n- Customer Admin: Can list reports for their customer only\n\nAuthentication: X-API-Key header required (reseller-level)\n\nQuery Parameters:\n- customer_id (optional): Filter reports for specific customer\n\nResponse:\n- 200: Success with list of reports\n- 401: Unauthorized (invalid/missing API key)\n- 500: Server error\n\nExample:\nGET /api/v1/reports\nGET /api/v1/reports?customer_id=123e4567-e89b-12d3-a456-426614174000\nX-API-Key: <reseller_api_key>\n\nResponse:\n{\n    \"data\": [\n        {\n            \"id\": \"uuid\",\n            \"reseller_id\": \"uuid\",\n            \"customer_id\": \"uuid\",\n            \"period_start\": \"2024-01-01\",\n            \"period_end\": \"2024-01-31\",\n            \"csv_path\": \"reseller_id/customer_id/2024-01-01_to_2024-01-31.csv\",\n            \"csv_size_bytes\": 15234,\n            \"generated_by\": \"api_key\",\n            \"generated_at\": \"2024-02-01T10:30:00Z\"\n        }\n    ],\n    \"status\": \"success\"\n}\n\nRequirements: 3.1, 3.2, 3.3, 3.4, 6.1, 6.2","operationId":"list_reports_api_v1_reports_get","parameters":[{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"Filter by customer ID","title":"Customer Id"},"description":"Filter by customer ID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reports/{report_id}/download":{"get":{"tags":["reports"],"summary":"Download Report","description":"Download report CSV with headers in requested language.\n\nGenerates CSV file with headers translated to the specified language.\nThe report data is fetched from the database and CSV is generated\non-demand with localized headers.\n\nAccess Control:\n- Reseller: Can download reports for all their customers\n- Customer Admin: Can download reports for their customer only (Post-MVP)\n\nAuthentication: X-API-Key header OR JWT token required (reseller-level)\n\nParameters:\n- report_id: UUID of the report\n- locale: Language code for CSV headers ('en' or 'es', default: 'en')\n\nResponse:\n- 200: CSV file with localized headers\n- 403: Access denied (customer doesn't belong to reseller)\n- 404: Report not found\n- 500: Generation error\n\nExample:\nGET /api/v1/reports/{report_id}/download?locale=es\nX-API-Key: <reseller_api_key>\n\nResponse:\nContent-Type: text/csv\nContent-Disposition: attachment; filename=\"report_2024-01-01_2024-01-31_es.csv\"\n\nNombre del Residente,ID del Dispositivo,Lectura Inicial (m³),...\nJohn Doe,383936306c4b5880,10.5,...\n\nRequirements: 1.2, 1.14, 5.3, 5.4, 5.5, 5.6, 6.3, 6.4, 6.5\nDynamic Translation: Headers translated on-demand based on locale parameter","operationId":"download_report_api_v1_reports__report_id__download_get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Report Id"}},{"name":"locale","in":"query","required":false,"schema":{"type":"string","pattern":"^(en|es)$","description":"Language for CSV headers (en or es)","default":"en","title":"Locale"},"description":"Language for CSV headers (en or es)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reports/{report_id}":{"delete":{"tags":["reports"],"summary":"Delete Report","description":"Delete a report and its associated CSV file from storage.\n\nRemoves the report metadata from the database and deletes the CSV file\nfrom Supabase Storage.\n\nAccess Control:\n- Reseller: Can delete reports for all their customers\n- Customer Admin: Can delete reports for their customer only (Post-MVP)\n\nAuthentication: X-API-Key header required (reseller-level)\n\nParameters:\n- report_id: UUID of the report to delete\n\nResponse:\n- 200: Success\n- 403: Access denied (customer doesn't belong to reseller)\n- 404: Report not found\n- 500: Server error\n\nExample:\nDELETE /api/v1/reports/{report_id}\nX-API-Key: <reseller_api_key>\n\nResponse:\n{\n    \"data\": {\n        \"id\": \"uuid\",\n        \"deleted\": true\n    },\n    \"status\": \"success\"\n}","operationId":"delete_report_api_v1_reports__report_id__delete","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/unregistered-devices":{"get":{"tags":["unregistered-devices"],"summary":"List Unregistered Devices","description":"List unregistered devices for authenticated reseller\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Automatic Device Discovery:**\nThis endpoint lists devices that have sent data via ChirpStack webhook but\nare not yet registered in the system. Devices are automatically tracked when\nthey send their first webhook, enabling easy device onboarding.\n\n**Query Parameters:**\n- status: Optional filter by status (pending, registered, ignored)\n\n**Status Values:**\n- pending: Device needs attention (default for new devices)\n- registered: Device has been registered (historical record)\n- ignored: Device explicitly marked as not needing registration\n\n**Response Format:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"dev_eui\": \"aabbccddeeff0011\",\n      \"device_name\": \"AABBCCDDEEFF0011 24h\",\n      \"tenant_name\": \"my-community\",\n      \"first_seen_at\": \"2024-01-01T12:00:00Z\",\n      \"last_seen_at\": \"2024-01-01T14:30:00Z\",\n      \"message_count\": 10,\n      \"status\": \"pending\"\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success (returns empty array if no unregistered devices)\n- 401: Invalid or missing API key\n- 400: Invalid status parameter\n\n**Multi-Tenant Security:**\n- Only devices belonging to the authenticated reseller are returned\n- Cross-reseller device access is prevented automatically\n\n**Ordering:**\nDevices are returned ordered by last_seen_at DESC (most recently active first)\n\n**Use Cases:**\n- View newly detected devices that need registration\n- Monitor device activity before registration\n- Identify devices that may need attention\n- Review historical device discovery patterns\n\nRequirements: 4.1, 4.2, 4.3, 4.4, 4.5","operationId":"list_unregistered_devices_api_v1_unregistered_devices_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(pending|registered|ignored)$"},{"type":"null"}],"title":"Status"}},{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by customer ID (matches tenant_name)","title":"Customer Id"},"description":"Filter by customer ID (matches tenant_name)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/unregistered-devices/{id}/register":{"post":{"tags":["unregistered-devices"],"summary":"Register Unregistered Device","description":"Register an unregistered device\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Quick Registration Workflow:**\nThis endpoint streamlines device onboarding by automatically copying the\ndev_eui and device_name from the unregistered device record. This reduces\nmanual data entry and prevents typos in critical device identifiers.\n\n**Path Parameters:**\n- id: Unregistered device UUID\n\n**Request Body:**\n```json\n{\n  \"name\": \"Water Meter 001\",\n  \"device_type\": \"water_meter\",\n  \"end_user_id\": \"456e7890-e89b-12d3-a456-426614174001\"\n}\n```\n\n**Optional Fields:**\n- device_type: Defaults to \"water_meter\" if not provided\n- end_user_id: Can be null to register without assignment\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"dev_eui\": \"aabbccddeeff0011\",\n    \"device_name\": \"AABBCCDDEEFF0011 24h\",\n    \"name\": \"Water Meter 001\",\n    \"device_type\": \"water_meter\",\n    \"status\": \"offline\",\n    \"battery_level\": null,\n    \"last_reading_at\": null,\n    \"is_active\": true,\n    \"end_user_id\": \"456e7890-e89b-12d3-a456-426614174001\",\n    \"created_at\": \"2024-01-01T15:00:00Z\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Device registered successfully\"\n}\n```\n\n**Response Codes:**\n- 200: Device registered successfully\n- 404: Unregistered device not found\n- 404: End user not found (if end_user_id provided)\n- 400: Device already registered (duplicate dev_eui)\n- 401: Invalid or missing API key\n\n**Multi-Tenant Security:**\n- Only unregistered devices belonging to the authenticated reseller can be registered\n- End users must belong to the same reseller\n- Cross-reseller registration is prevented automatically\n\n**Automatic Updates:**\n- Unregistered device status is updated to 'registered'\n- Device is created with status 'offline' (updates to 'online' on first data)\n- device_name is copied from unregistered device record\n- Future webhooks from this device will be accepted\n\n**Use Cases:**\n- Register newly detected device after field installation\n- Assign device to end user during registration\n- Bulk registration via API scripts\n\nRequirements: 5.1, 5.2, 5.3, 5.4, 5.5","operationId":"register_unregistered_device_api_v1_unregistered_devices__id__register_post","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterDevicePayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/unregistered-devices/{id}/status":{"put":{"tags":["unregistered-devices"],"summary":"Update Unregistered Device Status","description":"Update unregistered device status\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Status Management:**\nThis endpoint allows marking unregistered devices as 'ignored' to hide them\nfrom the default list view. This is useful for test devices, decommissioned\ndevices, or devices that you don't intend to register.\n\n**Path Parameters:**\n- id: Unregistered device UUID\n\n**Request Body:**\n```json\n{\n  \"status\": \"ignored\"\n}\n```\n\n**Status Values:**\n- pending: Device needs attention (default)\n- ignored: Device explicitly marked as not needing registration\n\n**Note:** The 'registered' status can only be set by the system via the\nregistration endpoint. Manual status updates are limited to 'pending' and\n'ignored' to maintain data integrity.\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"dev_eui\": \"aabbccddeeff0011\",\n    \"device_name\": \"AABBCCDDEEFF0011 24h\",\n    \"tenant_name\": \"my-community\",\n    \"first_seen_at\": \"2024-01-01T12:00:00Z\",\n    \"last_seen_at\": \"2024-01-01T14:30:00Z\",\n    \"message_count\": 10,\n    \"status\": \"ignored\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Status updated successfully\"\n}\n```\n\n**Response Codes:**\n- 200: Status updated successfully\n- 404: Unregistered device not found\n- 400: Invalid status value (Pydantic validation)\n- 401: Invalid or missing API key\n\n**Multi-Tenant Security:**\n- Only devices belonging to the authenticated reseller can be updated\n- Cross-reseller status updates are prevented automatically\n\n**Tracking Continues:**\nEven when marked as 'ignored', the system continues tracking message_count\nand last_seen_at. This allows you to monitor device activity even for\ndevices you don't intend to register.\n\n**Use Cases:**\n- Mark test devices as ignored during development\n- Hide decommissioned devices from active list\n- Temporarily ignore devices during troubleshooting\n- Restore devices to pending status when ready to register\n\nRequirements: 6.1, 6.2, 6.3, 6.4, 6.5","operationId":"update_unregistered_device_status_api_v1_unregistered_devices__id__status_put","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStatusPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/thresholds/devices/{device_id}":{"put":{"tags":["thresholds"],"summary":"Set Device Threshold","description":"Set or update daily consumption threshold for a device (reseller admin)\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Threshold Validation:**\n- Minimum: 10 liters (prevents alarm fatigue)\n- Maximum: 100,000 liters (sanity check)\n- Must be positive number\n\n**Request Body:**\n```json\n{\n  \"daily_threshold_liters\": 150.0,\n  \"enabled\": true\n}\n```\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"device_id\": \"uuid\",\n    \"reseller_id\": \"uuid\",\n    \"daily_threshold_liters\": 150.0,\n    \"enabled\": true,\n    \"created_at\": \"2024-01-01T00:00:00Z\",\n    \"updated_at\": \"2024-01-01T00:00:00Z\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Threshold configured successfully\"\n}\n```\n\n**Response Codes:**\n- 200: Threshold configured successfully\n- 400: Invalid threshold value\n- 401: Invalid or missing API key\n- 404: Device not found or doesn't belong to reseller\n\n**Error Codes:**\n- DEVICE_NOT_FOUND: Device doesn't exist or doesn't belong to reseller\n- THRESHOLD_TOO_LOW: Threshold below 10L minimum\n- THRESHOLD_TOO_HIGH: Threshold above 100,000L maximum\n- INVALID_THRESHOLD: Threshold is not a positive number\n\n**Requirements:** 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9","operationId":"set_device_threshold_api_v1_thresholds_devices__device_id__put","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThresholdCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["thresholds"],"summary":"Get Device Threshold","description":"Get threshold configuration for a specific device (reseller admin)\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"device_id\": \"uuid\",\n    \"reseller_id\": \"uuid\",\n    \"daily_threshold_liters\": 150.0,\n    \"enabled\": true,\n    \"created_at\": \"2024-01-01T00:00:00Z\",\n    \"updated_at\": \"2024-01-01T00:00:00Z\"\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success\n- 401: Invalid or missing API key\n- 404: Device or threshold not found\n\n**Error Codes:**\n- DEVICE_NOT_FOUND: Device doesn't exist or doesn't belong to reseller\n- THRESHOLD_NOT_CONFIGURED: No threshold configured for this device\n\n**Requirements:** 2.1, 2.2, 2.3, 2.4","operationId":"get_device_threshold_api_v1_thresholds_devices__device_id__get","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/thresholds":{"get":{"tags":["thresholds"],"summary":"List Thresholds","description":"List all threshold configurations for the reseller\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Response Format:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"device_id\": \"uuid\",\n      \"device_name\": \"Water Meter 001\",\n      \"dev_eui\": \"383936306c4b5880\",\n      \"reseller_id\": \"uuid\",\n      \"daily_threshold_liters\": 150.0,\n      \"enabled\": true,\n      \"created_at\": \"2024-01-01T00:00:00Z\",\n      \"updated_at\": \"2024-01-01T00:00:00Z\"\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success (returns empty array if no thresholds configured)\n- 401: Invalid or missing API key\n\n**Requirements:** 2.5, 2.6","operationId":"list_thresholds_api_v1_thresholds_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}}},"/api/v1/thresholds/bulk-update":{"post":{"tags":["thresholds"],"summary":"Bulk Update Thresholds","description":"Enable or disable thresholds for multiple devices at once (reseller admin)\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Request Body:**\n```json\n{\n  \"device_ids\": [\"uuid1\", \"uuid2\", \"uuid3\"],\n  \"enabled\": false\n}\n```\n\n**Limits:**\n- Maximum 100 device IDs per request\n- Only updates devices belonging to authenticated reseller\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"updated_count\": 3,\n    \"device_ids\": [\"uuid1\", \"uuid2\", \"uuid3\"]\n  },\n  \"status\": \"success\",\n  \"message\": \"3 thresholds updated successfully\"\n}\n```\n\n**Response Codes:**\n- 200: Thresholds updated successfully\n- 400: No devices found or invalid request\n- 401: Invalid or missing API key\n\n**Error Codes:**\n- NO_DEVICES_FOUND: None of the provided device IDs belong to this reseller\n\n**Requirements:** 9.1, 9.2, 9.3, 9.4, 9.5, 9.6","operationId":"bulk_update_thresholds_api_v1_thresholds_bulk_update_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThresholdBulkUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/user/thresholds/devices/{device_id}":{"put":{"tags":["user-thresholds"],"summary":"Set User Device Threshold","description":"Set or update daily consumption threshold for an assigned device (end user)\n\nEnd users can only configure thresholds for devices assigned to them.\n\n**Authentication:** Requires Authorization header with Bearer token (Supabase Auth)\n\n**Threshold Validation:**\n- Minimum: 10 liters (prevents alarm fatigue)\n- Maximum: 100,000 liters (sanity check)\n- Must be positive number\n\n**Request Body:**\n```json\n{\n  \"daily_threshold_liters\": 150.0,\n  \"enabled\": true\n}\n```\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"device_id\": \"uuid\",\n    \"reseller_id\": \"uuid\",\n    \"daily_threshold_liters\": 150.0,\n    \"enabled\": true,\n    \"created_at\": \"2024-01-01T00:00:00Z\",\n    \"updated_at\": \"2024-01-01T00:00:00Z\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Threshold configured successfully\"\n}\n```\n\n**Response Codes:**\n- 200: Threshold configured successfully\n- 400: Invalid threshold value\n- 401: Invalid or missing authentication token\n- 404: Device not found or not assigned to user\n\n**Error Codes:**\n- DEVICE_NOT_FOUND: Device doesn't exist or is not assigned to this end user\n- THRESHOLD_TOO_LOW: Threshold below 10L minimum\n- THRESHOLD_TOO_HIGH: Threshold above 100,000L maximum\n- INVALID_THRESHOLD: Threshold is not a positive number\n\n**Requirements:** 1B.1, 1B.2, 1B.3, 1B.4, 1B.5, 1B.6, 1B.7, 1B.8, 1B.9, 1B.10","operationId":"set_user_device_threshold_api_v1_user_thresholds_devices__device_id__put","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThresholdCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["user-thresholds"],"summary":"Get User Device Threshold","description":"Get threshold configuration for an assigned device (end user)\n\n**Authentication:** Requires Authorization header with Bearer token (Supabase Auth)\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"device_id\": \"uuid\",\n    \"reseller_id\": \"uuid\",\n    \"daily_threshold_liters\": 150.0,\n    \"enabled\": true,\n    \"created_at\": \"2024-01-01T00:00:00Z\",\n    \"updated_at\": \"2024-01-01T00:00:00Z\"\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success\n- 401: Invalid or missing authentication token\n- 404: Device not assigned or threshold not configured\n\n**Error Codes:**\n- DEVICE_NOT_FOUND: Device doesn't exist or is not assigned to this end user\n- THRESHOLD_NOT_CONFIGURED: No threshold configured for this device\n\n**Requirements:** 2B.1, 2B.2, 2B.3, 2B.4","operationId":"get_user_device_threshold_api_v1_user_thresholds_devices__device_id__get","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/user/thresholds":{"get":{"tags":["user-thresholds"],"summary":"List User Thresholds","description":"List threshold configurations for all assigned devices (end user)\n\n**Authentication:** Requires Authorization header with Bearer token (Supabase Auth)\n\n**Response Format:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"device_id\": \"uuid\",\n      \"device_name\": \"Water Meter 001\",\n      \"dev_eui\": \"383936306c4b5880\",\n      \"reseller_id\": \"uuid\",\n      \"daily_threshold_liters\": 150.0,\n      \"enabled\": true,\n      \"created_at\": \"2024-01-01T00:00:00Z\",\n      \"updated_at\": \"2024-01-01T00:00:00Z\"\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success (returns empty array if no thresholds configured)\n- 401: Invalid or missing authentication token\n\n**Requirements:** 2B.5, 2B.6","operationId":"list_user_thresholds_api_v1_user_thresholds_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}}},"/api/v1/alarms":{"get":{"tags":["alarms"],"summary":"List Alarms","description":"List consumption alarms for the authenticated reseller\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Query Parameters:**\n- `unacknowledged_only` (optional): If true, return only unacknowledged alarms (default: false)\n- `customer_id` (optional): Filter alarms by customer ID\n\n**Response Format:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"device_id\": \"uuid\",\n      \"device_name\": \"Water Meter 001\",\n      \"dev_eui\": \"383936306c4b5880\",\n      \"reseller_id\": \"uuid\",\n      \"alarm_date\": \"2024-01-01\",\n      \"daily_consumption_liters\": 200.5,\n      \"threshold_liters\": 150.0,\n      \"exceeded_by_liters\": 50.5,\n      \"acknowledged\": false,\n      \"acknowledged_at\": null,\n      \"acknowledged_by\": null,\n      \"created_at\": \"2024-01-02T00:30:00Z\"\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Behavior:**\n- Returns up to 100 most recent alarms\n- Ordered by alarm_date DESC (most recent first)\n- Includes device details (name, dev_eui) via JOIN\n- Filters by authenticated reseller_id for multi-tenant isolation\n- Optionally filters by customer_id when provided\n\n**Response Codes:**\n- 200: Success (returns empty array if no alarms)\n- 401: Invalid or missing API key\n\n**Requirements:** 6.1, 6.2, 6.3, 6.4, 6.5","operationId":"list_alarms_api_v1_alarms_get","parameters":[{"name":"unacknowledged_only","in":"query","required":false,"schema":{"type":"boolean","description":"If true, return only unacknowledged alarms","default":false,"title":"Unacknowledged Only"},"description":"If true, return only unacknowledged alarms"},{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter alarms by customer ID","title":"Customer Id"},"description":"Filter alarms by customer ID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alarms/devices/{device_id}":{"get":{"tags":["alarms"],"summary":"List Device Alarms","description":"List alarms for a specific device\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Response Format:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid\",\n      \"device_id\": \"uuid\",\n      \"device_name\": \"Water Meter 001\",\n      \"dev_eui\": \"383936306c4b5880\",\n      \"reseller_id\": \"uuid\",\n      \"alarm_date\": \"2024-01-01\",\n      \"daily_consumption_liters\": 200.5,\n      \"threshold_liters\": 150.0,\n      \"exceeded_by_liters\": 50.5,\n      \"acknowledged\": false,\n      \"acknowledged_at\": null,\n      \"acknowledged_by\": null,\n      \"created_at\": \"2024-01-02T00:30:00Z\"\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Behavior:**\n- Returns all alarms for the specified device\n- Ordered by alarm_date DESC (most recent first)\n- Includes device details (name, dev_eui) via JOIN\n- Filters by authenticated reseller_id for multi-tenant isolation\n\n**Response Codes:**\n- 200: Success (returns empty array if no alarms for device)\n- 401: Invalid or missing API key\n- 404: Device not found or doesn't belong to reseller\n\n**Error Codes:**\n- DEVICE_NOT_FOUND: Device doesn't exist or doesn't belong to reseller\n\n**Requirements:** 6.6","operationId":"list_device_alarms_api_v1_alarms_devices__device_id__get","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alarms/{alarm_id}/acknowledge":{"post":{"tags":["alarms"],"summary":"Acknowledge Alarm","description":"Acknowledge an alarm (mark as reviewed)\n\n**Authentication:** Requires X-API-Key header (reseller-level) or JWT token (end user)\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"device_id\": \"uuid\",\n    \"device_name\": \"Water Meter 001\",\n    \"dev_eui\": \"383936306c4b5880\",\n    \"reseller_id\": \"uuid\",\n    \"alarm_date\": \"2024-01-01\",\n    \"daily_consumption_liters\": 200.5,\n    \"threshold_liters\": 150.0,\n    \"exceeded_by_liters\": 50.5,\n    \"acknowledged\": true,\n    \"acknowledged_at\": \"2024-01-02T10:30:00Z\",\n    \"acknowledged_by\": \"uuid\",\n    \"created_at\": \"2024-01-02T00:30:00Z\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Alarm acknowledged successfully\"\n}\n```\n\n**Behavior:**\n- Sets acknowledged to true\n- Sets acknowledged_at to current timestamp\n- Sets acknowledged_by to authenticated user_id (if end user)\n- Idempotent: Returns 200 if already acknowledged\n- Filters by authenticated reseller_id for multi-tenant isolation\n\n**Response Codes:**\n- 200: Alarm acknowledged successfully (or already acknowledged)\n- 401: Invalid or missing API key/token\n- 404: Alarm not found or doesn't belong to reseller\n\n**Error Codes:**\n- ALARM_NOT_FOUND: Alarm doesn't exist or doesn't belong to reseller\n\n**Requirements:** 7.1, 7.2, 7.3, 7.4, 7.5, 7.6","operationId":"acknowledge_alarm_api_v1_alarms__alarm_id__acknowledge_post","parameters":[{"name":"alarm_id","in":"path","required":true,"schema":{"type":"string","title":"Alarm Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alarms/bulk-acknowledge":{"post":{"tags":["alarms"],"summary":"Bulk Acknowledge Alarms","description":"Acknowledge multiple alarms at once\n\n**Authentication:** Requires X-API-Key header (reseller-level) or JWT token (end user)\n\n**Request Body:**\n```json\n{\n  \"alarm_ids\": [\"uuid1\", \"uuid2\", \"uuid3\"]\n}\n```\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"acknowledged_count\": 3,\n    \"alarm_ids\": [\"uuid1\", \"uuid2\", \"uuid3\"]\n  },\n  \"status\": \"success\",\n  \"message\": \"Successfully acknowledged 3 alarms\"\n}\n```\n\n**Behavior:**\n- Sets acknowledged to true for all specified alarms\n- Sets acknowledged_at to current timestamp\n- Sets acknowledged_by to authenticated user_id (if end user)\n- Idempotent: Includes already acknowledged alarms in count\n- Filters by authenticated reseller_id for multi-tenant isolation\n- Only acknowledges alarms that belong to the authenticated reseller\n\n**Response Codes:**\n- 200: Alarms acknowledged successfully\n- 400: Invalid request body (missing or empty alarm_ids)\n- 401: Invalid or missing API key/token\n\n**Error Codes:**\n- INVALID_REQUEST: Missing or empty alarm_ids array\n\n**Requirements:** 4.7","operationId":"bulk_acknowledge_alarms_api_v1_alarms_bulk_acknowledge_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}}},"/api/v1/alarms/stats":{"get":{"tags":["alarms"],"summary":"Get Alarm Stats","description":"Get overall alarm statistics for the authenticated reseller\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"total_alarms\": 150,\n    \"unacknowledged_alarms\": 25,\n    \"devices_with_alarms\": 12,\n    \"alarms_last_30_days\": 45\n  },\n  \"status\": \"success\"\n}\n```\n\n**Statistics:**\n- `total_alarms`: Total count of all alarms for this reseller\n- `unacknowledged_alarms`: Count of alarms not yet acknowledged\n- `devices_with_alarms`: Count of distinct devices that have alarms\n- `alarms_last_30_days`: Count of alarms created in the last 30 days\n\n**Behavior:**\n- All statistics filtered by authenticated reseller_id\n- Efficient single query with aggregations\n\n**Response Codes:**\n- 200: Success\n- 401: Invalid or missing API key\n\n**Requirements:** 8.1, 8.2","operationId":"get_alarm_stats_api_v1_alarms_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}}},"/api/v1/alarms/stats/devices":{"get":{"tags":["alarms"],"summary":"Get Device Alarm Stats","description":"Get per-device alarm statistics for the authenticated reseller\n\n**Authentication:** Requires X-API-Key header (reseller-level)\n\n**Response Format:**\n```json\n{\n  \"data\": [\n    {\n      \"device_id\": \"uuid\",\n      \"device_name\": \"Water Meter 001\",\n      \"dev_eui\": \"383936306c4b5880\",\n      \"alarm_count\": 15,\n      \"last_alarm_date\": \"2024-01-15\",\n      \"average_exceeded_by_liters\": 45.5\n    }\n  ],\n  \"status\": \"success\"\n}\n```\n\n**Statistics per Device:**\n- `device_id`: UUID of the device\n- `device_name`: Human-readable device name\n- `dev_eui`: Device EUI (16 hex characters)\n- `alarm_count`: Total number of alarms for this device\n- `last_alarm_date`: Date of the most recent alarm\n- `average_exceeded_by_liters`: Average amount over threshold across all alarms\n\n**Behavior:**\n- Groups alarms by device_id\n- Joins with devices table for device details\n- Ordered by alarm_count DESC (devices with most alarms first)\n- Filtered by authenticated reseller_id\n\n**Response Codes:**\n- 200: Success (returns empty array if no alarms)\n- 401: Invalid or missing API key\n\n**Requirements:** 8.3, 8.4, 8.5, 8.6","operationId":"get_device_alarm_stats_api_v1_alarms_stats_devices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}}},"/api/v1/analytics/leaks":{"get":{"tags":["analytics"],"summary":"Get Leak Alarms","description":"Get leak alarms with filtering support.\n\n**Authentication:** Requires X-API-Key header or JWT token\n\n**Multi-tenant:** Only returns alarms for authenticated reseller\n\n**Filtering Options:**\n- customer_id: Filter by specific customer\n- status: Filter by alarm status (active, investigating, resolved)\n- severity: Filter by leak severity (info, warning, critical)\n- start_date: Filter alarms after this date (ISO 8601)\n- end_date: Filter alarms before this date (ISO 8601)\n- limit: Maximum number of results (1-100, default 50)\n\n**Requirements:** 6.1, 6.3, 6.5","operationId":"get_leak_alarms_api_v1_analytics_leaks_get","parameters":[{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"Filter by customer ID","title":"Customer Id"},"description":"Filter by customer ID"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/AlarmStatus"},{"type":"null"}],"description":"Filter by alarm status","title":"Status"},"description":"Filter by alarm status"},{"name":"severity","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/LeakSeverity"},{"type":"null"}],"description":"Filter by leak severity","title":"Severity"},"description":"Filter by leak severity"},{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter alarms after this date","title":"Start Date"},"description":"Filter alarms after this date"},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter alarms before this date","title":"End Date"},"description":"Filter alarms before this date"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Maximum number of alarms to return","default":50,"title":"Limit"},"description":"Maximum number of alarms to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/zones":{"get":{"tags":["analytics"],"summary":"Get Customer Zones","description":"Get zone structures with customer hierarchy data.\n\n**Authentication:** Requires X-API-Key header or JWT token\n\n**Multi-tenant:** Only returns zones for authenticated reseller\n\n**Operating Modes:**\n- Customer Mode: Provide customer_id to get zones for specific customer\n- Direct Mode: Omit customer_id to get zones for all reseller devices (no customer hierarchy)\n\n**Zone Structure:** Returns hierarchical zone data including:\n- Zone metadata (level, root device, children devices)\n- Device counts and status information\n- Last analysis timestamps\n- Active alarm counts per zone\n\n**Requirements:** 6.2, 6.4, 6.5","operationId":"get_customer_zones_api_v1_analytics_zones_get","parameters":[{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"Filter by specific customer ID (NULL for Direct Mode)","title":"Customer Id"},"description":"Filter by specific customer ID (NULL for Direct Mode)"},{"name":"use_cache","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to use cached zone data","default":true,"title":"Use Cache"},"description":"Whether to use cached zone data"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/alarms/{alarm_id}/status":{"put":{"tags":["analytics"],"summary":"Update Alarm Status","description":"Update alarm investigation status.\n\n**Authentication:** Requires X-API-Key header or JWT token\n\n**Multi-tenant:** Only allows updating alarms owned by authenticated reseller\n\n**Status Workflow:** active → investigating → resolved\n\n**Request Body:**\n- status: New alarm status (active, investigating, resolved)\n- investigation_notes: Optional notes about the investigation\n- resolved_by: Optional identifier of who resolved the alarm (required for resolved status)\n\n**Requirements:** 6.3, 6.4, 6.5","operationId":"update_alarm_status_api_v1_analytics_alarms__alarm_id__status_put","parameters":[{"name":"alarm_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"Alarm ID to update","title":"Alarm Id"},"description":"Alarm ID to update"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlarmStatusUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/devices-needing-attention":{"get":{"tags":["analytics"],"summary":"Get Devices Needing Attention","description":"Get prioritized list of devices requiring immediate attention.\n\n**Authentication:** Requires X-API-Key header or JWT token\n\n**Priority Algorithm:**\n1. Offline devices (>24 hours) - Critical\n2. Low battery devices (<3.0V) - High  \n3. High consumption anomalies (>50% increase) - High/Medium\n4. Maintenance mode devices - Medium\n\n**Multi-tenant:** Only returns devices for authenticated reseller\n\n**Requirements:** 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8","operationId":"get_devices_needing_attention_api_v1_analytics_devices_needing_attention_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Maximum number of devices to return","default":50,"title":"Limit"},"description":"Maximum number of devices to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/devices-needing-attention/{device_id}/acknowledge":{"put":{"tags":["analytics"],"summary":"Acknowledge Device Attention","description":"Acknowledge a device attention item (temporarily remove from list).\n\n**Authentication:** Requires X-API-Key header or JWT token\n\n**Multi-tenant:** Only allows acknowledging devices owned by authenticated reseller\n\n**Note:** This is a placeholder endpoint. In a full implementation, this would\nstore acknowledgment state in a separate table to temporarily hide devices\nfrom the attention list while the issue persists.\n\n**Requirements:** 7.8","operationId":"acknowledge_device_attention_api_v1_analytics_devices_needing_attention__device_id__acknowledge_put","parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/customer-uptime":{"get":{"tags":["analytics"],"summary":"Get Customer Uptime","description":"Get customer uptime metrics and trends.\n\n**Authentication:** Requires X-API-Key header or JWT token\n\n**Multi-tenant:** Only returns data for authenticated reseller\n\n**Requirements:** 8.1, 8.2, 8.3","operationId":"get_customer_uptime_api_v1_analytics_customer_uptime_get","parameters":[{"name":"time_range","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":7,"description":"Time range in days","default":30,"title":"Time Range"},"description":"Time range in days"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/customer-growth":{"get":{"tags":["analytics"],"summary":"Get Customer Growth","description":"Get customer growth and business metrics.\n\n**Authentication:** Requires X-API-Key header or JWT token\n\n**Multi-tenant:** Only returns data for authenticated reseller\n\n**Requirements:** 11.1, 11.2, 11.3","operationId":"get_customer_growth_api_v1_analytics_customer_growth_get","parameters":[{"name":"time_range","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":7,"description":"Time range in days","default":30,"title":"Time Range"},"description":"Time range in days"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/sla-compliance":{"get":{"tags":["analytics"],"summary":"Get Sla Compliance","description":"Get SLA compliance metrics and customer uptime tracking.\n\n**Authentication:** Requires X-API-Key header or JWT token\n\n**Multi-tenant:** Only returns data for authenticated reseller\n\n**SLA Calculation:** Based on device uptime and reporting intervals\n- Within SLA: ≥95% uptime (configurable)\n- Approaching Breach: 90-95% uptime  \n- Breaching SLA: <90% uptime\n\n**Requirements:** 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8","operationId":"get_sla_compliance_api_v1_analytics_sla_compliance_get","parameters":[{"name":"time_range","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":7,"description":"Time range in days","default":30,"title":"Time Range"},"description":"Time range in days"},{"name":"sla_target","in":"query","required":false,"schema":{"type":"number","maximum":100.0,"minimum":80.0,"description":"SLA uptime target percentage","default":95.0,"title":"Sla Target"},"description":"SLA uptime target percentage"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/devices-per-day":{"get":{"tags":["analytics"],"summary":"Get Devices Per Day","description":"Get count of distinct devices reporting data per day.\n\n**Authentication:** Requires X-API-Key header or JWT token\n\n**Multi-tenant:** Only returns data for authenticated reseller\n\n**Parameters:**\n- days: 7 or 30 (default 7)\n- customer_id: Optional filter by customer (legacy)\n- customer: Optional filter — UUID for specific customer, 'unassociated' for devices without customer\n\n**Returns:** Daily device counts for the period, with 0-filled missing dates.\n\n**Requirements:** 3.1, 3.5, 3.6, 3.7","operationId":"get_devices_per_day_api_v1_analytics_devices_per_day_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","description":"Number of days (7 or 30)","default":7,"title":"Days"},"description":"Number of days (7 or 30)"},{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"Filter by customer ID","title":"Customer Id"},"description":"Filter by customer ID"},{"name":"customer","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by customer: UUID or 'unassociated'","title":"Customer"},"description":"Filter by customer: UUID or 'unassociated'"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/top-consumers":{"get":{"tags":["analytics"],"summary":"Get Top Consumers","description":"Get top consuming devices for a customer.\n\nDelegates to the unified consumption_calculator service. Uses LEAF\nchildren only (same scope as comparison chart and leak detection)\nto avoid double-counting intermediate parent meters.\n\n**Authentication:** Requires X-API-Key header or JWT token\n**Multi-tenant:** Only returns data for authenticated reseller\n**Requirements:** TopConsumersChart, 4.1","operationId":"get_top_consumers_api_v1_analytics_top_consumers_get","parameters":[{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"Customer ID (required)","title":"Customer Id"},"description":"Customer ID (required)"},{"name":"days","in":"query","required":false,"schema":{"type":"integer","description":"Number of days (7 or 30)","default":30,"title":"Days"},"description":"Number of days (7 or 30)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","description":"Maximum number of devices to return (max 10)","default":10,"title":"Limit"},"description":"Maximum number of devices to return (max 10)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/consumption-per-day":{"get":{"tags":["analytics"],"summary":"Get Consumption Per Day","description":"Get total water consumption per day for a customer.\n\nDelegates to the unified consumption_calculator service so that the\nnumbers are guaranteed to match the comparison chart and top-consumers.\n\n**Authentication:** Requires X-API-Key header or JWT token\n**Multi-tenant:** Only returns data for authenticated reseller\n**Requirements:** 4.1–4.9 (ConsumptionChart)","operationId":"get_consumption_per_day_api_v1_analytics_consumption_per_day_get","parameters":[{"name":"customer_id","in":"query","required":true,"schema":{"type":"string","format":"uuid","description":"Customer ID (required)","title":"Customer Id"},"description":"Customer ID (required)"},{"name":"days","in":"query","required":false,"schema":{"type":"integer","description":"Number of days (7 or 30)","default":7,"title":"Days"},"description":"Number of days (7 or 30)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/consumption-comparison":{"get":{"tags":["analytics"],"summary":"Get Consumption Comparison","description":"Compare daily consumption between principal meter(s) and leaf-child meters.\n\nDelegates to the unified consumption_calculator service so that the\nprincipal totals match the consumption-per-day endpoint exactly.\n\n**Time-alignment:** child readings capped at principal's last timestamp.\n**Leaf children:** hierarchy_level > 0 AND no children of their own.\n\n**Returns:** Array of { date, principal, childrenSum } per day.","operationId":"get_consumption_comparison_api_v1_analytics_consumption_comparison_get","parameters":[{"name":"customer_id","in":"query","required":true,"schema":{"type":"string","format":"uuid","description":"Customer ID (required)","title":"Customer Id"},"description":"Customer ID (required)"},{"name":"days","in":"query","required":false,"schema":{"type":"integer","description":"Number of days (7 or 30)","default":7,"title":"Days"},"description":"Number of days (7 or 30)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/snn-anomaly-detection":{"get":{"tags":["analytics"],"summary":"Snn Anomaly Detection","description":"SNN-based anomaly detection for IoT device time-series data.\n\nProxies to a dedicated SNN microservice that runs torch + snntorch,\nkeeping the main API lean and memory-efficient.\n\nUses a Leaky Integrate-and-Fire (LIF) spiking neural network to detect\nsustained anomalous behavior in device readings.\n\nCurrently implemented data sources:\n- water_consumption: Hourly consumption deltas from cumulative meter readings\n\nQuery parameters allow tuning the LIF neuron sensitivity per request.","operationId":"snn_anomaly_detection_api_v1_analytics_snn_anomaly_detection_get","parameters":[{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"Customer ID (None for Direct Mode)","title":"Customer Id"},"description":"Customer ID (None for Direct Mode)"},{"name":"sensor_type","in":"query","required":false,"schema":{"type":"string","description":"Sensor type: water_consumption, temperature, humidity, energy","default":"water_consumption","title":"Sensor Type"},"description":"Sensor type: water_consumption, temperature, humidity, energy"},{"name":"baseline_days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":7,"description":"Days of history for baseline normalization","default":60,"title":"Baseline Days"},"description":"Days of history for baseline normalization"},{"name":"analysis_hours","in":"query","required":false,"schema":{"type":"integer","maximum":2160,"minimum":24,"description":"Hours of recent data to scan for anomalies","default":168,"title":"Analysis Hours"},"description":"Hours of recent data to scan for anomalies"},{"name":"beta","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":0.99,"minimum":0.1},{"type":"null"}],"description":"LIF membrane decay (0-1). Higher = slower leak. Default per sensor type.","title":"Beta"},"description":"LIF membrane decay (0-1). Higher = slower leak. Default per sensor type."},{"name":"threshold","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":10.0,"minimum":1.0},{"type":"null"}],"description":"LIF spike threshold. Higher = fewer false positives.","title":"Threshold"},"description":"LIF spike threshold. Higher = fewer false positives."},{"name":"num_steps","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":50,"minimum":5},{"type":"null"}],"description":"Integration timesteps per data point.","title":"Num Steps"},"description":"Integration timesteps per data point."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/device-anomaly-alerts":{"get":{"tags":["analytics"],"summary":"Get Device Anomaly Alerts","description":"Get per-device anomaly alerts.\n\nThese are separate from zone-based leak alarms. Three detectors:\n- zscore: single-day consumption spike above baseline (all devices)\n- zero_streak: consecutive zero readings (principal meters only)\n- cusum: sustained elevation above baseline mean (all devices)","operationId":"get_device_anomaly_alerts_api_v1_analytics_device_anomaly_alerts_get","parameters":[{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"Filter by customer ID","title":"Customer Id"},"description":"Filter by customer ID"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by status: active, investigating, resolved","title":"Status"},"description":"Filter by status: active, investigating, resolved"},{"name":"detector","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by detector: zscore, zero_streak, cusum","title":"Detector"},"description":"Filter by detector: zscore, zero_streak, cusum"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Max results","default":50,"title":"Limit"},"description":"Max results"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/device-anomaly-alerts/{alert_id}/status":{"put":{"tags":["analytics"],"summary":"Update Device Anomaly Alert Status","description":"Update a device anomaly alert status (active → investigating → resolved).","operationId":"update_device_anomaly_alert_status_api_v1_analytics_device_anomaly_alerts__alert_id__status_put","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"Alert ID to update","title":"Alert Id"},"description":"Alert ID to update"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlarmStatusUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reseller/team":{"get":{"tags":["team"],"summary":"List team members","description":"Get all team members for the authenticated reseller.\n    \n    Returns team members ordered by: owner first, then by join date.\n    Excludes removed members (status='removed').\n    \n    **Authentication:** X-API-Key header or JWT token required\n    \n    **Response Format:**\n    ```json\n    {\n      \"data\": [\n        {\n          \"id\": \"uuid\",\n          \"reseller_id\": \"uuid\",\n          \"user_id\": \"uuid\",\n          \"email\": \"owner@company.com\",\n          \"status\": \"active\",\n          \"is_owner\": true,\n          \"invited_by\": null,\n          \"invited_at\": null,\n          \"joined_at\": \"2024-01-01T00:00:00Z\",\n          \"created_at\": \"2024-01-01T00:00:00Z\"\n        }\n      ],\n      \"status\": \"success\"\n    }\n    ```\n    \n    **Response Codes:**\n    - 200: Success\n    - 401: Invalid or missing authentication","operationId":"list_team_members_api_v1_reseller_team_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamMemberListResponse"}}}}}}},"/api/v1/reseller/team/invite":{"post":{"tags":["team"],"summary":"Invite team member","description":"Invite a new team member by email.\n    \n    Creates a pending team member record and sends a magic link email\n    to the invited user.\n    \n    **Authentication:** X-API-Key header or JWT token required\n    \n    **Request Body:**\n    ```json\n    {\n      \"email\": \"colleague@company.com\"\n    }\n    ```\n    \n    **Response Format:**\n    ```json\n    {\n      \"data\": {\n        \"id\": \"uuid\",\n        \"reseller_id\": \"uuid\",\n        \"user_id\": null,\n        \"email\": \"colleague@company.com\",\n        \"status\": \"pending\",\n        \"is_owner\": false,\n        \"invited_by\": \"uuid\",\n        \"invited_at\": \"2024-01-01T10:00:00Z\",\n        \"joined_at\": null,\n        \"created_at\": \"2024-01-01T10:00:00Z\"\n      },\n      \"status\": \"success\",\n      \"message\": \"Invite sent successfully\"\n    }\n    ```\n    \n    **Response Codes:**\n    - 200: Invite sent successfully\n    - 400: EMAIL_ALREADY_MEMBER - Email is already an active team member\n    - 400: INVITE_ALREADY_PENDING - An invite is already pending for this email\n    - 401: Invalid or missing authentication","operationId":"invite_team_member_api_v1_reseller_team_invite_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamMemberCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamMemberResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reseller/team/{member_id}":{"delete":{"tags":["team"],"summary":"Remove team member","description":"Remove a team member from the reseller account.\n    \n    Sets the member's status to 'removed' and revokes their access.\n    Cannot remove the account owner.\n    \n    **Authentication:** X-API-Key header or JWT token required\n    \n    **Response Format:**\n    ```json\n    {\n      \"status\": \"success\",\n      \"message\": \"Team member removed\"\n    }\n    ```\n    \n    **Response Codes:**\n    - 200: Team member removed successfully\n    - 403: CANNOT_REMOVE_OWNER - Cannot remove the account owner\n    - 404: MEMBER_NOT_FOUND - Team member not found\n    - 401: Invalid or missing authentication","operationId":"remove_team_member_api_v1_reseller_team__member_id__delete","parameters":[{"name":"member_id","in":"path","required":true,"schema":{"type":"string","title":"Member Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Remove Team Member Api V1 Reseller Team  Member Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reseller/team/{member_id}/resend":{"post":{"tags":["team"],"summary":"Resend invite","description":"Resend the magic link email to a pending team member.\n    \n    Updates the invited_at timestamp and sends a new magic link.\n    Only works for members with status='pending'.\n    \n    **Authentication:** X-API-Key header or JWT token required\n    \n    **Response Format:**\n    ```json\n    {\n      \"status\": \"success\",\n      \"message\": \"Invite resent\"\n    }\n    ```\n    \n    **Response Codes:**\n    - 200: Invite resent successfully\n    - 400: MEMBER_NOT_PENDING - Team member is not in pending status\n    - 404: MEMBER_NOT_FOUND - Team member not found\n    - 401: Invalid or missing authentication","operationId":"resend_invite_api_v1_reseller_team__member_id__resend_post","parameters":[{"name":"member_id","in":"path","required":true,"schema":{"type":"string","title":"Member Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resend Invite Api V1 Reseller Team  Member Id  Resend Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reseller/team/{member_id}/cancel":{"delete":{"tags":["team"],"summary":"Cancel invite","description":"Cancel a pending invite by deleting the team member record.\n    \n    Only works for members with status='pending'.\n    \n    **Authentication:** X-API-Key header or JWT token required\n    \n    **Response Format:**\n    ```json\n    {\n      \"status\": \"success\",\n      \"message\": \"Invite canceled\"\n    }\n    ```\n    \n    **Response Codes:**\n    - 200: Invite canceled successfully\n    - 400: MEMBER_NOT_PENDING - Team member is not in pending status\n    - 404: MEMBER_NOT_FOUND - Team member not found\n    - 401: Invalid or missing authentication","operationId":"cancel_invite_api_v1_reseller_team__member_id__cancel_delete","parameters":[{"name":"member_id","in":"path","required":true,"schema":{"type":"string","title":"Member Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cancel Invite Api V1 Reseller Team  Member Id  Cancel Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/verify-token":{"post":{"tags":["auth"],"summary":"Verify Token","description":"Verify an invitation token server-side via the Supabase Admin API.\n\nThis endpoint is the fallback for when client-side verifyOtp() fails\nbecause the token_hash originated from the Admin generate_link API\n(which produces tokens incompatible with client-side verifyOtp).\n\nThe Supabase Admin API POST /auth/v1/verify accepts { token_hash, type }\nand returns session tokens when the token is valid.\n\nThis endpoint is PUBLIC (no auth required) because the user has no\nsession yet — they're clicking an invitation link for the first time.\n\nRequest Body:\n    token_hash: The hashed token from the invitation email URL\n    type: The token type (typically \"magiclink\")\n\nReturns:\n    On success: { access_token, refresh_token, user }\n    On invalid/expired token: 401 error\n    On missing parameters: 400 error\n\nRequirements: 2.1, 2.2","operationId":"verify_token_api_v1_auth_verify_token_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyTokenRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/accept-invite":{"post":{"tags":["auth"],"summary":"Accept Invite Endpoint","description":"Accept a team invite for a user.\n\nThis endpoint is called by the frontend auth callback after successful\nauthentication to link the user to the reseller team.\n\nRequest Body:\n    user_id: UUID of the authenticated user from Supabase Auth\n    email: Email address of the authenticated user\n    reseller_id: Reseller ID from the invite link\n    \nReturns:\n    Success message with reseller_id\n    \nRequirements:\n    - Backend 4.1: Check for pending invites matching email\n    - Backend 4.2: Update team member record if invite found\n    - Backend 4.3: Update user's JWT claims with reseller_id","operationId":"accept_invite_endpoint_api_v1_auth_accept_invite_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInviteRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/callback":{"post":{"tags":["auth"],"summary":"Auth Callback","description":"Handle authentication callback from Supabase Auth with domain-aware redirects.\n\nThis endpoint is called after successful authentication (magic link or OAuth).\nIt checks for pending team invites, accepts them if found, and redirects users\nto their appropriate reseller subdomain.\n\nQuery Parameters:\n    user_id: UUID of the authenticated user from Supabase Auth\n    email: Email address of the authenticated user\n    reseller_id: Optional reseller ID from the invite link\n    \nReturns:\n    Redirect to the appropriate dashboard based on user role and domain context\n    \nRequirements:\n    - Backend 4.1: Check for pending invites matching email\n    - Backend 4.2: Update team member record if invite found\n    - Backend 4.3: Update user's JWT claims with reseller_id\n    - Backend 4.4: Redirect to appropriate dashboard\n    - Backend 4.5: Handle normal auth flow if no invite\n    - 3.2: Redirect users from app.datakubo.com to their reseller subdomain\n    - 6.4: Preserve original destination URL in redirect\n    - 9.3: Handle users without reseller association (stay on app.datakubo.com)","operationId":"auth_callback_api_v1_auth_callback_post","parameters":[{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User ID from Supabase Auth","title":"User Id"},"description":"User ID from Supabase Auth"},{"name":"email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User email from Supabase Auth","title":"Email"},"description":"User email from Supabase Auth"},{"name":"reseller_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Reseller ID from invite link","title":"Reseller Id"},"description":"Reseller ID from invite link"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["auth"],"summary":"Auth Callback Get","description":"Handle GET requests to auth callback (for magic link redirects).\n\nThis is the same as the POST handler but for GET requests,\nwhich is what Supabase Auth typically uses for magic link redirects.\n\nQuery Parameters:\n    user_id: UUID of the authenticated user from Supabase Auth\n    email: Email address of the authenticated user\n    reseller_id: Optional reseller ID from the invite link\n    \nReturns:\n    Redirect to the appropriate dashboard based on user role","operationId":"auth_callback_get_api_v1_auth_callback_get","parameters":[{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User ID from Supabase Auth","title":"User Id"},"description":"User ID from Supabase Auth"},{"name":"email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User email from Supabase Auth","title":"Email"},"description":"User email from Supabase Auth"},{"name":"reseller_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Reseller ID from invite link","title":"Reseller Id"},"description":"Reseller ID from invite link"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/user-redirect":{"post":{"tags":["auth"],"summary":"Get User Redirect","description":"Get the appropriate redirect URL for a user based on their reseller associations.\n\nThis endpoint is called by the frontend to determine where to redirect users\nafter authentication, particularly when they're on app.datakubo.com and should\nbe redirected to their reseller subdomain.\n\nRequest Body:\n    user_id: UUID of the authenticated user\n    email: Email address of the authenticated user\n    \nReturns:\n    Redirect URL if user has reseller association, null otherwise\n    \nRequirements:\n    - 3.2: Redirect users from app.datakubo.com to their reseller subdomain\n    - 9.3: Handle users without reseller association (stay on app.datakubo.com)","operationId":"get_user_redirect_api_v1_auth_user_redirect_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRedirectRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/custom-domains/":{"get":{"tags":["custom-domains"],"summary":"List Custom Domains","description":"List all custom domains for the authenticated reseller.\n\nReturns:\n    List of custom domains with verification status and SSL status\n    \nRequirements:\n    - 8.1: Display current custom domains with verification status","operationId":"list_custom_domains_api_v1_custom_domains__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}},"post":{"tags":["custom-domains"],"summary":"Add Custom Domain","description":"Add a new custom domain for the authenticated reseller.\n\nArgs:\n    request: Domain creation request with domain name\n    \nReturns:\n    Created custom domain with verification instructions\n    \nRequirements:\n    - 8.1: Accept requests from custom domain and load reseller context\n    - 8.5: Show DNS configuration instructions for manual setup","operationId":"add_custom_domain_api_v1_custom_domains__post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomDomainCreateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/custom-domains/{domain_id}/verification-instructions":{"get":{"tags":["custom-domains"],"summary":"Get Verification Instructions","description":"Get DNS verification instructions for a custom domain.\n\nArgs:\n    domain_id: UUID of the custom domain\n    \nReturns:\n    DNS verification instructions with TXT record details\n    \nRequirements:\n    - 8.5: Show DNS configuration instructions for manual setup","operationId":"get_verification_instructions_api_v1_custom_domains__domain_id__verification_instructions_get","parameters":[{"name":"domain_id","in":"path","required":true,"schema":{"type":"string","title":"Domain Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/custom-domains/{domain_id}/verify":{"post":{"tags":["custom-domains"],"summary":"Verify Domain Ownership","description":"Verify domain ownership by checking DNS TXT record.\n\nArgs:\n    domain_id: UUID of the custom domain to verify\n    \nReturns:\n    Verification result with updated domain status\n    \nRequirements:\n    - 8.4: Update domain status based on verification results","operationId":"verify_domain_ownership_api_v1_custom_domains__domain_id__verify_post","parameters":[{"name":"domain_id","in":"path","required":true,"schema":{"type":"string","title":"Domain Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/custom-domains/{domain_id}":{"delete":{"tags":["custom-domains"],"summary":"Remove Custom Domain","description":"Remove a custom domain from the reseller's account.\n\nArgs:\n    domain_id: UUID of the custom domain to remove\n    \nReturns:\n    Confirmation of domain removal\n    \nRequirements:\n    - 8.1: Manage custom domains (including removal)","operationId":"remove_custom_domain_api_v1_custom_domains__domain_id__delete","parameters":[{"name":"domain_id","in":"path","required":true,"schema":{"type":"string","title":"Domain Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/customer-invitations":{"get":{"tags":["customer-invitations"],"summary":"List Customer Invitations","description":"List all customer invitations for the authenticated reseller.\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Purpose:**\nFetches all customer invitations for the reseller. Used by the frontend\ncustomers table to display Admin Access column and invitation count badges.\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"invitations\": [\n      {\n        \"id\": \"uuid\",\n        \"customer_id\": \"uuid\",\n        \"reseller_id\": \"uuid\",\n        \"email\": \"admin@example.com\",\n        \"status\": \"pending\",\n        \"invited_at\": \"2024-01-15T10:30:00Z\",\n        \"accepted_at\": null,\n        \"onboarding_completed_at\": null\n      }\n    ]\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success (may return empty list)\n- 401: Invalid or missing authentication\n- 500: Internal server error","operationId":"list_customer_invitations_api_v1_customer_invitations_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}},"post":{"tags":["customer-invitations"],"summary":"Send Customer Invitation","description":"Send customer admin invitation via Supabase Auth magic link\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Purpose:**\nCreates a customer invitation record and sends a magic link email to the specified\nemail address. The recipient can authenticate via the magic link and gain read-only\naccess to the customer's reports.\n\n**Request Body:**\n```json\n{\n  \"customer_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n  \"email\": \"admin@example.com\"\n}\n```\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"invitation\": {\n      \"id\": \"uuid\",\n      \"customer_id\": \"uuid\",\n      \"email\": \"admin@example.com\",\n      \"status\": \"pending\",\n      \"invited_at\": \"2024-01-15T10:30:00Z\"\n    }\n  },\n  \"status\": \"success\",\n  \"message\": \"Invitation sent successfully\"\n}\n```\n\n**Response Codes:**\n- 200: Success (invitation sent)\n- 400: Invalid customer_id format or email format\n- 401: Invalid or missing authentication\n- 403: Customer belongs to different reseller\n- 404: Customer not found\n- 409: Invitation already exists for this customer\n- 500: Failed to send invitation email\n\n**Multi-Tenant Security:**\n- Automatically filters by authenticated reseller_id\n- Returns 403 if customer belongs to different reseller\n- Only one invitation per customer (enforced by unique constraint)\n\n**Magic Link Behavior:**\n- Sends email via Supabase Auth with custom redirect URL\n- Redirect URL includes customer_id for invitation acceptance\n- Magic link expires after 24 hours (Supabase default)\n- User can authenticate and gain customer admin access\n\n**Use Cases:**\n- Reseller invites customer admin to view reports\n- Customer admin receives email with magic link\n- Customer admin authenticates and gains read-only access\n\n**Example:**\n```bash\ncurl -X POST https://api.datakubo.com/api/v1/customer-invitations       -H \"X-API-Key: reseller_abc123\"       -H \"Content-Type: application/json\"       -d '{\"customer_id\":\"123e4567-e89b-12d3-a456-426614174000\",\"email\":\"admin@example.com\"}'\n```","operationId":"send_customer_invitation_api_v1_customer_invitations_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendInvitationRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/customer-invitations/{customer_id}":{"get":{"tags":["customer-invitations"],"summary":"Get Customer Invitation","description":"Get invitation status for a specific customer\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Purpose:**\nFetches the current invitation status for a customer admin. Used by the frontend\nto display the CustomerAccessCard component on the Reports page.\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"invitation\": {\n      \"id\": \"uuid\",\n      \"customer_id\": \"uuid\",\n      \"email\": \"admin@example.com\",\n      \"status\": \"pending\",\n      \"invited_at\": \"2024-01-15T10:30:00Z\",\n      \"accepted_at\": null\n    }\n  },\n  \"status\": \"success\"\n}\n```\n\n**Status Values:**\n- `pending`: Invitation sent but not yet accepted\n- `active`: Invitation accepted, customer admin has access\n- `revoked`: Access has been revoked\n\n**Response Codes:**\n- 200: Success (invitation found)\n- 404: No invitation exists for this customer (frontend handles as 'not_invited')\n- 401: Invalid or missing authentication\n- 403: Customer belongs to different reseller\n- 400: Invalid customer_id format\n\n**Multi-Tenant Security:**\n- Automatically filters by authenticated reseller_id\n- Returns 403 if customer belongs to different reseller\n- Returns 404 if customer doesn't exist\n\n**Use Cases:**\n- Display invitation status on Customer Reports page\n- Determine which actions are available (invite, resend, revoke)\n- Show customer admin email and invitation date\n\n**Example:**\n```bash\ncurl -X GET https://api.datakubo.com/api/v1/customer-invitations/123e4567-e89b-12d3-a456-426614174000       -H \"X-API-Key: reseller_abc123\"\n```","operationId":"get_customer_invitation_api_v1_customer_invitations__customer_id__get","parameters":[{"name":"customer_id","in":"path","required":true,"schema":{"type":"string","title":"Customer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/customer-invitations/{customer_id}/history":{"get":{"tags":["customer-invitations"],"summary":"Get Invitation History","description":"Fetch invitation history events for a specific customer.\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Purpose:**\nReturns the audit trail of invitation events for a customer, used on the\ncustomer detail page (`/admin/customers/[id]`).\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"events\": [\n      {\n        \"id\": \"uuid\",\n        \"invitation_id\": \"uuid\",\n        \"customer_id\": \"uuid\",\n        \"event_type\": \"invited\",\n        \"performed_by\": \"uuid\",\n        \"performed_by_email\": \"admin@reseller.com\",\n        \"created_at\": \"2024-01-15T10:30:00Z\"\n      }\n    ]\n  },\n  \"status\": \"success\"\n}\n```\n\n**Response Codes:**\n- 200: Success (may return empty list)\n- 400: Invalid customer_id format\n- 404: Customer not found or belongs to different reseller\n- 401: Invalid or missing authentication\n- 500: Internal server error","operationId":"get_invitation_history_api_v1_customer_invitations__customer_id__history_get","parameters":[{"name":"customer_id","in":"path","required":true,"schema":{"type":"string","title":"Customer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/customer-invitations/bulk-resend":{"post":{"tags":["customer-invitations"],"summary":"Bulk Resend Invitations","description":"Bulk resend stale pending invitations (older than 7 days).\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Purpose:**\nFinds pending invitations older than 7 days for the reseller and resends\nmagic links. Processes a maximum of 10 invitations per batch (oldest first).\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"resent_count\": 3,\n    \"failed_count\": 0,\n    \"total_stale\": 5\n  },\n  \"status\": \"success\",\n  \"message\": \"Resent invitations to 3 customers\"\n}\n```\n\n**Response Codes:**\n- 200: Success (even if 0 invitations resent)\n- 401: Invalid or missing authentication\n- 500: Internal server error","operationId":"bulk_resend_invitations_api_v1_customer_invitations_bulk_resend_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}}}}},"/api/v1/customer-invitations/{invitation_id}/resend":{"post":{"tags":["customer-invitations"],"summary":"Resend Customer Invitation","description":"Resend magic link for a pending customer invitation\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Purpose:**\nResends the magic link email for a pending invitation. Updates the invited_at\ntimestamp to track when the invitation was last sent.\n\n**Response Format:**\n```json\n{\n  \"data\": {},\n  \"status\": \"success\",\n  \"message\": \"Invitation resent successfully\"\n}\n```\n\n**Response Codes:**\n- 200: Success (invitation resent)\n- 400: Invalid invitation_id format\n- 401: Invalid or missing authentication\n- 403: Invitation belongs to different reseller\n- 404: Invitation not found\n- 409: Invitation is not pending (already active or revoked)\n- 500: Failed to send email\n\n**Multi-Tenant Security:**\n- Automatically filters by authenticated reseller_id\n- Returns 403 if invitation belongs to different reseller\n\n**Use Cases:**\n- Customer admin didn't receive original email\n- Magic link expired (24 hours)\n- Reseller wants to remind customer admin to accept\n\n**Example:**\n```bash\ncurl -X POST https://api.datakubo.com/api/v1/customer-invitations/123e4567-e89b-12d3-a456-426614174000/resend       -H \"X-API-Key: reseller_abc123\"\n```","operationId":"resend_customer_invitation_api_v1_customer_invitations__invitation_id__resend_post","parameters":[{"name":"invitation_id","in":"path","required":true,"schema":{"type":"string","title":"Invitation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/customer-invitations/{invitation_id}":{"delete":{"tags":["customer-invitations"],"summary":"Revoke Customer Invitation","description":"Revoke customer admin access\n\n**Authentication:** Requires X-API-Key header or JWT token (reseller-level)\n\n**Purpose:**\nRevokes customer admin access by updating the invitation status to 'revoked'.\nThis prevents the customer admin from accessing reports.\n\n**Response Format:**\n```json\n{\n  \"data\": {},\n  \"status\": \"success\",\n  \"message\": \"Access revoked successfully\"\n}\n```\n\n**Response Codes:**\n- 200: Success (access revoked)\n- 400: Invalid invitation_id format\n- 401: Invalid or missing authentication\n- 403: Invitation belongs to different reseller\n- 404: Invitation not found\n- 500: Internal server error\n\n**Multi-Tenant Security:**\n- Automatically filters by authenticated reseller_id\n- Returns 403 if invitation belongs to different reseller\n\n**Use Cases:**\n- Reseller wants to remove customer admin access\n- Customer admin no longer needs access\n- Security concern requires immediate access revocation\n\n**Example:**\n```bash\ncurl -X DELETE https://api.datakubo.com/api/v1/customer-invitations/123e4567-e89b-12d3-a456-426614174000       -H \"X-API-Key: reseller_abc123\"\n```","operationId":"revoke_customer_invitation_api_v1_customer_invitations__invitation_id__delete","parameters":[{"name":"invitation_id","in":"path","required":true,"schema":{"type":"string","title":"Invitation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/customer-invitations/accept":{"post":{"tags":["customer-invitations"],"summary":"Accept Customer Invitation","description":"Accept customer invitation after magic link authentication\n\n**Authentication:** Requires valid Supabase Auth session (JWT token)\n\n**Purpose:**\nActivates a pending customer invitation after the user authenticates via magic link.\nUpdates the invitation status to 'active', records the accepted timestamp, and\nupdates the user's JWT claims to include role='customer' and customer_id.\n\n**Request Body:**\n```json\n{\n  \"email\": \"admin@example.com\"\n}\n```\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"customer_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n    \"redirect_url\": \"/customers/123e4567-e89b-12d3-a456-426614174000/reports\"\n  },\n  \"status\": \"success\",\n  \"message\": \"Invitation accepted successfully\"\n}\n```\n\n**Response Codes:**\n- 200: Success (invitation accepted)\n- 400: Invalid email format\n- 401: Invalid or missing authentication\n- 404: No pending invitation found for this email\n- 500: Internal server error\n\n**Flow:**\n1. User clicks magic link in email\n2. Supabase Auth authenticates user\n3. Frontend calls this endpoint with user's email\n4. Backend finds pending invitation matching email\n5. Backend updates invitation status to 'active'\n6. Backend updates user metadata with role='customer' and customer_id\n7. Frontend redirects to /customers/{customer_id}/reports\n\n**Use Cases:**\n- Customer admin accepts invitation via magic link\n- User gains read-only access to customer reports\n- JWT token updated with customer role and customer_id\n\n**Example:**\n```bash\ncurl -X POST https://api.datakubo.com/api/v1/customer-invitations/accept       -H \"Authorization: Bearer <jwt_token>\"       -H \"Content-Type: application/json\"       -d '{\"email\":\"admin@example.com\"}'\n```","operationId":"accept_customer_invitation_api_v1_customer_invitations_accept_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInvitationRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/customer-invitations/{invitation_id}/onboarding":{"patch":{"tags":["customer-invitations"],"summary":"Complete Onboarding","description":"Mark onboarding as completed for a customer invitation.\n\n**Authentication:** Requires X-API-Key header or JWT token\n\n**Purpose:**\nSets the `onboarding_completed_at` timestamp on an active invitation.\nCalled when the customer admin clicks \"Get Started\" on the welcome modal.\nThis prevents the welcome modal from showing automatically on subsequent logins.\n\n**Response Format:**\n```json\n{\n  \"data\": {\n    \"invitation\": {\n      \"id\": \"uuid\",\n      \"customer_id\": \"uuid\",\n      \"email\": \"admin@example.com\",\n      \"status\": \"active\",\n      \"onboarding_completed_at\": \"2024-01-15T10:30:00Z\"\n    }\n  },\n  \"status\": \"success\",\n  \"message\": \"Onboarding completed\"\n}\n```\n\n**Response Codes:**\n- 200: Success (onboarding marked as completed)\n- 400: Invalid invitation_id format\n- 401: Invalid or missing authentication\n- 403: Invitation belongs to different reseller\n- 404: Invitation not found\n- 409: Invitation is not active (must be active to complete onboarding)\n- 500: Internal server error","operationId":"complete_onboarding_api_v1_customer_invitations__invitation_id__onboarding_patch","parameters":[{"name":"invitation_id","in":"path","required":true,"schema":{"type":"string","title":"Invitation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/customer-invitations/auto-resend":{"post":{"tags":["customer-invitations"],"summary":"Auto Resend Expired Link","description":"Auto-resend magic link for expired invitation links.\n\n**Authentication:** None required (public endpoint)\n\n**Purpose:**\nWhen a customer admin clicks an expired magic link, the frontend calls this\nendpoint to automatically resend a new magic link. Enforces a 5-minute cooldown\nto prevent abuse.\n\n**Security:**\n- No authentication required (user's link just expired, they have no session)\n- Requires a valid pending invitation to exist for the email (prevents abuse)\n- 5-minute cooldown prevents spam\n\n**Request Body:**\n```json\n{\n  \"email\": \"admin@example.com\"\n}\n```\n\n**Response Format (resent):**\n```json\n{\n  \"data\": {\n    \"status\": \"resent\",\n    \"reseller_name\": \"AquaLinks\",\n    \"reseller_logo\": \"https://...\",\n    \"reseller_color\": \"#3B82F6\"\n  },\n  \"status\": \"success\",\n  \"message\": \"New magic link sent\"\n}\n```\n\n**Response Format (cooldown):**\n```json\n{\n  \"data\": {\n    \"status\": \"cooldown\",\n    \"reseller_name\": \"AquaLinks\",\n    \"reseller_logo\": \"https://...\",\n    \"reseller_color\": \"#3B82F6\"\n  },\n  \"status\": \"success\",\n  \"message\": \"A new link was recently sent\"\n}\n```\n\n**Response Codes:**\n- 200: Success (resent or cooldown)\n- 404: No pending invitation found for this email\n- 500: Internal server error\n\n**Validates: Requirement 7.10**","operationId":"auto_resend_expired_link_api_v1_customer_invitations_auto_resend_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoResendRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/stripe/create-checkout-session":{"post":{"tags":["stripe"],"summary":"Create Checkout Session","description":"Create a Stripe Checkout session for a new subscription.\n\nResolves the correct price ID based on the selected tier and whether\nthe reseller is an early adopter. Creates a Stripe Customer on first\ncheckout and stores the customer ID in the database.\n\nAuthentication: Requires X-API-Key or Bearer token.\n\nRequest Body:\n    tier: \"starter\" or \"growth\" (founding is not self-service)\n\nReturns:\n    {\"url\": \"<stripe checkout url>\"}\n\nRequirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 8.6","operationId":"create_checkout_session_api_v1_stripe_create_checkout_session_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/stripe/create-portal-session":{"post":{"tags":["stripe"],"summary":"Create Portal Session","description":"Create a Stripe Customer Portal session for subscription management.\n\nAllows resellers to upgrade, downgrade, cancel, update payment method,\nand view invoice history through Stripe's hosted portal.\n\nAuthentication: Requires X-API-Key or Bearer token.\n\nReturns:\n    {\"url\": \"<stripe portal url>\"}\n\nErrors:\n    400 NO_STRIPE_CUSTOMER: Reseller has never subscribed.\n\nRequirements: 3.1, 3.3, 3.4","operationId":"create_portal_session_api_v1_stripe_create_portal_session_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/stripe/manage-subscription":{"post":{"tags":["stripe"],"summary":"Manage Subscription","description":"Subscription management endpoint.\n\nAlways opens the Stripe Customer Portal where the reseller can:\n- Choose a plan (if on trial/canceled)\n- Upgrade/downgrade their plan\n- Cancel their subscription\n- Update payment method\n- View invoice history\n\nCreates a Stripe Customer on the fly if one doesn't exist yet.\n\nAuthentication: Requires X-API-Key or Bearer token.\n\nReturns:\n    {\"action\": \"portal\", \"url\": \"<stripe portal url>\"}","operationId":"manage_subscription_api_v1_stripe_manage_subscription_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/stripe/create-customer-session":{"post":{"tags":["stripe"],"summary":"Create Customer Session","description":"Create a Stripe Customer Session for the Pricing Table embed.\n\nThe Pricing Table needs a customer-session-client-secret to link\nthe checkout to an existing Stripe Customer. This endpoint creates\n(or reuses) a Stripe Customer for the reseller and returns the\nclient_secret.\n\nAuthentication: Requires X-API-Key or Bearer token.\n\nReturns:\n    {\"client_secret\": \"<customer session client secret>\"}","operationId":"create_customer_session_api_v1_stripe_create_customer_session_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/stripe/toggle-addon":{"post":{"tags":["stripe"],"summary":"Toggle Addon","description":"Enable or disable an add-on on the reseller's existing subscription.\n\nAdds or removes a subscription item (e.g. Custom Domain at €10/mo)\non the reseller's Stripe subscription and syncs the state to the\nreseller_addons table.\n\nAuthentication: Requires X-API-Key or Bearer token.\n\nRequest Body:\n    addon: \"custom_domain\"\n    enabled: true | false\n\nReturns:\n    {\"status\": \"success\", \"addon\": \"custom_domain\", \"enabled\": true/false}\n\nErrors:\n    400 NO_ACTIVE_SUBSCRIPTION: No stripe_subscription_id on reseller.\n    400 SUBSCRIPTION_NOT_ACTIVE: Subscription status is not 'active'.\n\nRequirements: 11.2, 11.3, 11.4, 11.5","operationId":"toggle_addon_api_v1_stripe_toggle_addon_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToggleAddonRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/stripe/webhook":{"post":{"tags":["stripe"],"summary":"Stripe Webhook","description":"Handle Stripe webhook events. No auth dependency — uses signature verification.\n\nProcesses subscription lifecycle events and syncs state to the database.\nAlways returns HTTP 200 to acknowledge receipt (Stripe retries on non-2xx).\n\nEvents handled:\n    - checkout.session.completed: Store Stripe IDs (not active yet)\n    - invoice.paid: Activate subscription, sync tier\n    - customer.subscription.updated: Sync status, tier, cancel state, add-ons\n    - customer.subscription.deleted: Cancel subscription, deactivate add-ons\n    - invoice.payment_failed: Set past_due status\n\nRequirements: 2.1–2.13","operationId":"stripe_webhook_api_v1_stripe_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/hardware-alarms":{"get":{"tags":["hardware-alarms"],"summary":"List Hardware Alarms","description":"List hardware alarm events for the authenticated reseller.\n\nReturns an append-only event log of alarm state transitions from device firmware.\nMost recent events first. No resolve workflow — devices self-clear alarms.\n\n**Filters:** customer_id, device_id, alarm_type, state, severity\n**Pagination:** limit + offset","operationId":"list_hardware_alarms_api_v1_hardware_alarms_get","parameters":[{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by customer ID","title":"Customer Id"},"description":"Filter by customer ID"},{"name":"device_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by device ID","title":"Device Id"},"description":"Filter by device ID"},{"name":"alarm_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by alarm type (e.g. burst, leakage)","title":"Alarm Type"},"description":"Filter by alarm type (e.g. burst, leakage)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by state: triggered or cleared","title":"State"},"description":"Filter by state: triggered or cleared"},{"name":"severity","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by severity: critical, warning, info","title":"Severity"},"description":"Filter by severity: critical, warning, info"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Max results (default 100, max 500)","default":100,"title":"Limit"},"description":"Max results (default 100, max 500)"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Offset for pagination","default":0,"title":"Offset"},"description":"Offset for pagination"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/hardware-alarms/stats":{"get":{"tags":["hardware-alarms"],"summary":"Hardware Alarm Stats","description":"Get summary statistics for hardware alarms.\n\nReturns counts by alarm type, severity, and currently active alarms\n(alarms where the most recent event for a device+alarm_type is 'triggered').","operationId":"hardware_alarm_stats_api_v1_hardware_alarms_stats_get","parameters":[{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by customer ID","title":"Customer Id"},"description":"Filter by customer ID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","description":"Root endpoint for basic connectivity test.","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/ping":{"get":{"summary":"Ping","description":"Simple ping endpoint that doesn't require database.","operationId":"ping_ping_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health Check","description":"Health check endpoint for Railway deployments and load balancers.\n\nRailway healthchecks require HTTP 200 for healthy deployments.\nReturns 200 for healthy/degraded states, 503 for critical failures.\n\nSee: https://docs.railway.com/reference/healthchecks","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AcceptInvitationRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"AcceptInvitationRequest","description":"Request model for accepting customer invitation"},"AcceptInviteRequest":{"properties":{"user_id":{"type":"string","title":"User Id"},"email":{"type":"string","title":"Email"},"reseller_id":{"type":"string","title":"Reseller Id"}},"type":"object","required":["user_id","email","reseller_id"],"title":"AcceptInviteRequest","description":"Request body for accepting a team invite"},"AlarmStatus":{"type":"string","enum":["active","investigating","resolved"],"title":"AlarmStatus","description":"Alarm lifecycle status.\n\nWorkflow: active → investigating → resolved"},"AlarmStatusUpdate":{"properties":{"status":{"$ref":"#/components/schemas/AlarmStatus","description":"New alarm status"},"investigation_notes":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Investigation Notes","description":"Notes about the investigation"},"resolved_by":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Resolved By","description":"Who resolved the alarm"}},"type":"object","required":["status"],"title":"AlarmStatusUpdate","description":"Request model for updating alarm status.\n\nRequirements: 5.4","example":{"investigation_notes":"Dispatched technician to check zone meters","status":"investigating"}},"ApiResponse":{"properties":{"data":{"title":"Data"},"status":{"type":"string","enum":["success","error"],"title":"Status"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["data","status"],"title":"ApiResponse","description":"Generic API response model.\n\nMirrors TypeScript ApiResponse<T> interface from shared/types.ts\n\nFields:\n    data: Response payload (can be any type)\n    status: Response status (\"success\" or \"error\")\n    message: Optional human-readable message","example":{"data":{"id":"123","name":"Device 1"},"message":"Operation completed successfully","status":"success"}},"AssignCustomerRequest":{"properties":{"customer_id":{"type":"string","format":"uuid","title":"Customer Id","description":"Customer ID to assign device to"}},"type":"object","required":["customer_id"],"title":"AssignCustomerRequest","description":"Request model for assigning a device to a customer.\n\nRequirements:\n- 4.1-4.8: Inline customer assignment for unassociated devices\n- 5.1: Assign customer to device\n\nFields:\n    customer_id: UUID of customer to assign device to","example":{"customer_id":"123e4567-e89b-12d3-a456-426614174000"}},"AssignUserRequest":{"properties":{"end_user_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"End User Id","description":"End user ID to assign device to (null to remove)"}},"type":"object","title":"AssignUserRequest","description":"Request model for assigning a device to an end user.\n\nRequirements:\n- 5b.1-5b.25: Inline user assignment for devices\n- 6.1: Assign user to device\n\nFields:\n    end_user_id: UUID of end user to assign device to (null to remove assignment)","examples":[{"description":"Assign device to user","value":{"end_user_id":"123e4567-e89b-12d3-a456-426614174000"}},{"description":"Remove user assignment","value":{}}]},"AutoResendRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"AutoResendRequest","description":"Request model for auto-resending expired magic link"},"BulkAssignCustomerRequest":{"properties":{"device_ids":{"items":{"type":"string","format":"uuid"},"type":"array","minItems":1,"title":"Device Ids","description":"List of device UUIDs to assign"},"customer_id":{"type":"string","format":"uuid","title":"Customer Id","description":"Customer ID to assign devices to"}},"type":"object","required":["device_ids","customer_id"],"title":"BulkAssignCustomerRequest","description":"Request model for bulk assigning devices to a customer.\n\nRequirements:\n- 6.3-6.5: Bulk assign to customer\n\nFields:\n    device_ids: List of device UUIDs to assign\n    customer_id: Customer UUID to assign devices to","example":{"customer_id":"456e7890-e89b-12d3-a456-426614174000","device_ids":["123e4567-e89b-12d3-a456-426614174000","123e4567-e89b-12d3-a456-426614174001"]}},"BulkAssignRequest":{"properties":{"device_ids":{"items":{"type":"string","format":"uuid"},"type":"array","minItems":1,"title":"Device Ids","description":"List of device UUIDs to assign"},"end_user_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"End User Id","description":"End user ID (null to unassign)"}},"type":"object","required":["device_ids"],"title":"BulkAssignRequest","description":"Request model for bulk assigning devices to an end user.\n\nFields:\n    device_ids: List of device UUIDs to assign\n    end_user_id: End user UUID to assign to (null to unassign)","example":{"device_ids":["123e4567-e89b-12d3-a456-426614174000","123e4567-e89b-12d3-a456-426614174001"],"end_user_id":"456e7890-e89b-12d3-a456-426614174000"}},"BulkAssignUserRequest":{"properties":{"device_ids":{"items":{"type":"string","format":"uuid"},"type":"array","minItems":1,"title":"Device Ids","description":"List of device UUIDs to assign"},"end_user_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"End User Id","description":"End user ID to assign devices to (null to remove)"}},"type":"object","required":["device_ids"],"title":"BulkAssignUserRequest","description":"Request model for bulk assigning devices to an end user.\n\nRequirements:\n- 6b.1-6b.8: Bulk user assignment\n\nFields:\n    device_ids: List of device UUIDs to assign\n    end_user_id: End user UUID to assign devices to (null to remove assignment)","examples":[{"description":"Assign multiple devices to user","value":{"device_ids":["123e4567-e89b-12d3-a456-426614174000","123e4567-e89b-12d3-a456-426614174001"],"end_user_id":"456e7890-e89b-12d3-a456-426614174000"}},{"description":"Remove user assignment from multiple devices","value":{"device_ids":["123e4567-e89b-12d3-a456-426614174000","123e4567-e89b-12d3-a456-426614174001"]}}]},"BulkDeleteRequest":{"properties":{"device_ids":{"items":{"type":"string","format":"uuid"},"type":"array","minItems":1,"title":"Device Ids","description":"List of device UUIDs to delete"}},"type":"object","required":["device_ids"],"title":"BulkDeleteRequest","description":"Request model for bulk deleting devices.\n\nFields:\n    device_ids: List of device UUIDs to delete","example":{"device_ids":["123e4567-e89b-12d3-a456-426614174000","123e4567-e89b-12d3-a456-426614174001"]}},"BulkStatusRequest":{"properties":{"device_ids":{"items":{"type":"string","format":"uuid"},"type":"array","minItems":1,"title":"Device Ids","description":"List of device UUIDs to update"},"status":{"type":"string","enum":["maintenance","auto"],"title":"Status","description":"Device status (maintenance or auto)"}},"type":"object","required":["device_ids","status"],"title":"BulkStatusRequest","description":"Request model for bulk updating device status.\n\nFields:\n    device_ids: List of device UUIDs to update\n    status: Status to set (maintenance or auto)","example":{"device_ids":["123e4567-e89b-12d3-a456-426614174000","123e4567-e89b-12d3-a456-426614174001"],"status":"maintenance"}},"CheckoutRequest":{"properties":{"tier":{"type":"string","enum":["starter","growth"],"title":"Tier"}},"type":"object","required":["tier"],"title":"CheckoutRequest"},"CustomDomainCreateRequest":{"properties":{"domain":{"type":"string","maxLength":253,"minLength":3,"title":"Domain"}},"type":"object","required":["domain"],"title":"CustomDomainCreateRequest","description":"Request model for creating a new custom domain.\n\nFields:\n    domain: Domain name to add (e.g., portal.aqualinks.es)","example":{"domain":"portal.aqualinks.es"}},"CustomerCreateRequest":{"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name"},"tenant_name":{"anyOf":[{"type":"string","maxLength":50,"minLength":1},{"type":"null"}],"title":"Tenant Name"},"admin_email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Admin Email"}},"type":"object","required":["name"],"title":"CustomerCreateRequest","description":"Customer creation request"},"CustomerUpdateRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"Name"},"admin_email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Admin Email"},"status":{"anyOf":[{"type":"string","enum":["active","inactive"]},{"type":"null"}],"title":"Status"}},"type":"object","title":"CustomerUpdateRequest","description":"Customer update request"},"DeviceAssignRequest":{"properties":{"end_user_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"End User Id","description":"End user ID (null to unassign)"}},"type":"object","title":"DeviceAssignRequest","description":"Request model for device assignment to end user.\n\nRequirement 7.2: Validate device assignment payload\n\nFields:\n    end_user_id: UUID of end user to assign device to (null to unassign)\n\nNote: initial_reading_m3 is automatically captured from the next webhook\ningestion after ownership change. This ensures accurate consumption\ncalculation for both previous and new owners.","examples":[{"description":"Assign device to user","value":{"end_user_id":"123e4567-e89b-12d3-a456-426614174000"}},{"description":"Unassign device","value":{}}]},"DeviceRegisterRequest":{"properties":{"dev_eui":{"type":"string","maxLength":16,"minLength":16,"title":"Dev Eui","description":"LoRaWAN Device EUI (16 hex characters)"},"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name","description":"User-friendly device name"},"device_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Type","description":"Device type","default":"water_meter"},"parent_device_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Parent Device Id","description":"Parent device UUID for hierarchy (optional)"},"hierarchy_level":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Hierarchy Level","description":"Hierarchy level (null = standalone, 0 = principal/root, 1+ = child)"},"latitude":{"anyOf":[{"type":"number","maximum":90.0,"minimum":-90.0},{"type":"null"}],"title":"Latitude","description":"GPS latitude in decimal degrees"},"longitude":{"anyOf":[{"type":"number","maximum":180.0,"minimum":-180.0},{"type":"null"}],"title":"Longitude","description":"GPS longitude in decimal degrees"},"altitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Altitude","description":"GPS altitude in meters above sea level"}},"type":"object","required":["dev_eui","name"],"title":"DeviceRegisterRequest","description":"Request model for device registration.\n\nRequirements:\n- 1.2, 1.8: Device registration with optional hierarchy fields\n- 5.2, 5.4: Validate device registration payload\n- 6.1, 6.2, 6.3: Optional parent_device_id and hierarchy_level fields\n\nFields:\n    dev_eui: LoRaWAN Device EUI (16 hexadecimal characters)\n    name: User-friendly device name\n    device_type: Type of device (default: \"water_meter\")\n    parent_device_id: Optional parent device UUID for hierarchy (default: None)\n    hierarchy_level: Hierarchy level (null = standalone, 0 = principal/root, 1+ = child at depth)","example":{"altitude":667.5,"dev_eui":"383936306c4b5880","device_type":"water_meter","latitude":40.416775,"longitude":-3.70379,"name":"Water Meter 001"}},"DeviceStatusRequest":{"properties":{"status":{"type":"string","enum":["maintenance","auto"],"title":"Status","description":"Device status (maintenance or auto)"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active","description":"Active flag for billing"}},"type":"object","required":["status"],"title":"DeviceStatusRequest","description":"Request model for manual device status update.\n\nRequirement 4b.1, 4b.2: Only accept \"maintenance\" or \"auto\" status values\n\nFields:\n    status: Device status (maintenance or auto)\n        - \"maintenance\": Manually set device to maintenance mode\n        - \"auto\": Calculate status based on last_reading_at (online if < 48h, offline otherwise)\n    is_active: Optional flag for billing (only active devices count)","example":{"is_active":false,"status":"maintenance"}},"DeviceUpdateRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"Name","description":"User-friendly device name"},"device_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Device Name","description":"Device name from ChirpStack"},"parent_device_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Parent Device Id","description":"Parent device UUID (null to remove parent)"},"hierarchy_level":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hierarchy Level","description":"Hierarchy level (null = standalone, 0 = principal/root, 1+ = child)"}},"type":"object","title":"DeviceUpdateRequest","description":"Request model for updating device details.\n\nFields:\n    name: User-friendly device name (optional)\n    device_name: Device name from ChirpStack (optional)\n    parent_device_id: Parent device UUID for hierarchy (optional, null to remove parent)\n    hierarchy_level: Hierarchy level (null = standalone, 0 = principal/root, 1+ = child)","examples":[{"description":"Update device name","value":{"name":"Water Meter 001 - Kitchen"}},{"description":"Update device name and set as child","value":{"hierarchy_level":1,"name":"Sub-Meter 001","parent_device_id":"123e4567-e89b-12d3-a456-426614174000"}},{"description":"Mark device as principal (root)","value":{"hierarchy_level":0}},{"description":"Make device standalone (remove from hierarchy)","value":{}}]},"EndUserCreateRequest":{"properties":{"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"},"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name"},"mobile_phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mobile Phone"},"customer_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Customer Id"}},"type":"object","required":["name"],"title":"EndUserCreateRequest","description":"Request model for creating a new end user.","example":{"customer_id":"456e7890-e89b-12d3-a456-426614174001","email":"resident@example.com","name":"John Doe"}},"EndUserUpdateRequest":{"properties":{"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"},"name":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"Name"},"mobile_phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mobile Phone"},"customer_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Customer Id"}},"type":"object","title":"EndUserUpdateRequest","description":"Request model for updating an end user.","example":{"customer_id":"456e7890-e89b-12d3-a456-426614174001","email":"newemail@example.com","name":"Jane Doe"}},"GenerateReportRequest":{"properties":{"customer_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Customer Id"},"preset":{"anyOf":[{"type":"string","enum":["last_month","last_3_months","since_last_report","custom"]},{"type":"null"}],"title":"Preset","default":"custom"},"start_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Start Date"},"end_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End Date"}},"type":"object","title":"GenerateReportRequest","description":"Request model for report generation"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HierarchyAssignment":{"properties":{"device_id":{"type":"string","format":"uuid","title":"Device Id","description":"Device UUID to update"},"hierarchy_level":{"type":"integer","minimum":0.0,"title":"Hierarchy Level","description":"0 = principal, 1+ = child"},"parent_device_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Parent Device Id","description":"Parent device UUID (required when hierarchy_level > 0)"}},"type":"object","required":["device_id","hierarchy_level"],"title":"HierarchyAssignment","description":"A single device hierarchy assignment within a batch operation.\n\nFields:\n    device_id: UUID of the device to update\n    hierarchy_level: 0 for principal, 1+ for child\n    parent_device_id: Required when hierarchy_level > 0"},"HierarchyBatchRequest":{"properties":{"assignments":{"items":{"$ref":"#/components/schemas/HierarchyAssignment"},"type":"array","minItems":1,"title":"Assignments","description":"List of hierarchy assignments"}},"type":"object","required":["assignments"],"title":"HierarchyBatchRequest","description":"Request model for batch hierarchy assignment.\n\nRequirements:\n- 2.1: Accept array of hierarchy assignments\n- 2.2: Validate all device_ids belong to the authenticated reseller\n- 2.3: Validate parent_device_id references a principal device (hierarchy_level == 0)\n\nFields:\n    assignments: List of device hierarchy assignments","example":{"assignments":[{"device_id":"123e4567-e89b-12d3-a456-426614174000","hierarchy_level":0},{"device_id":"123e4567-e89b-12d3-a456-426614174001","hierarchy_level":1,"parent_device_id":"123e4567-e89b-12d3-a456-426614174000"}]}},"LeakSeverity":{"type":"string","enum":["info","warning","critical"],"title":"LeakSeverity","description":"Leak severity classification based on consumption difference.\n\nClassification rules (from design.md):\n- INFO: Small anomaly, worth monitoring (difference < 10% or < 1 m³)\n- WARNING: Moderate leak (10-20% or 1-5 m³)\n- CRITICAL: Large leak (>20% or >5 m³)"},"RegisterDevicePayload":{"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name","description":"User-friendly device name"},"device_type":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Device Type","description":"Device type","default":"water_meter"},"end_user_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"End User Id","description":"End user ID to assign device to"}},"type":"object","required":["name"],"title":"RegisterDevicePayload","description":"Request payload for registering an unregistered device.\n\nUsed by POST /api/v1/unregistered-devices/{id}/register endpoint\n\nFields:\n    name: User-friendly device name\n    device_type: Device type (default: water_meter)\n    end_user_id: Optional end user assignment","example":{"device_type":"water_meter","end_user_id":"456e7890-e89b-12d3-a456-426614174001","name":"Water Meter 001"}},"ResellerSettingsRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"Name"},"logo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Url"},"primary_color":{"anyOf":[{"type":"string","pattern":"^#[0-9A-Fa-f]{6}$"},{"type":"null"}],"title":"Primary Color"},"tenant_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Tenant Name","description":"Default tenant name for webhook data ingestion when no customer exists"},"terminology":{"anyOf":[{"$ref":"#/components/schemas/ResellerTerminology"},{"type":"null"}]}},"type":"object","title":"ResellerSettingsRequest","description":"White-label settings update request"},"ResellerTerminology":{"properties":{"es":{"anyOf":[{"$ref":"#/components/schemas/ResellerTerminologyLabels"},{"type":"null"}]},"en":{"anyOf":[{"$ref":"#/components/schemas/ResellerTerminologyLabels"},{"type":"null"}]},"reseller":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reseller"},"customer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer"},"end_user":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"End User"},"device":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device"}},"type":"object","title":"ResellerTerminology","description":"Customizable UI terminology (Post-MVP feature).\n\nSupports two formats:\n1. Multi-language (new): { \"es\": {...}, \"en\": {...} }\n2. Legacy flat: { \"reseller\": \"...\", \"customer\": \"...\", ... }\n\nThe model accepts both and stores as-is in JSONB."},"ResellerTerminologyLabels":{"properties":{"reseller":{"type":"string","title":"Reseller","default":"Organization"},"customer":{"type":"string","title":"Customer","default":"Community"},"end_user":{"type":"string","title":"End User","default":"Resident"},"device":{"type":"string","title":"Device","default":"Water Meter"}},"type":"object","title":"ResellerTerminologyLabels","description":"Per-language terminology labels."},"ResellerTerminologyRequest":{"properties":{"reseller":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reseller"},"customer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer"},"end_user":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"End User"},"device":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device"}},"type":"object","title":"ResellerTerminologyRequest","description":"Terminology customization request (Post-MVP)"},"SendInvitationRequest":{"properties":{"customer_id":{"type":"string","title":"Customer Id"},"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["customer_id","email"],"title":"SendInvitationRequest","description":"Request model for sending customer admin invitation"},"SetParentRequest":{"properties":{"parent_device_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Parent Device Id","description":"Parent device UUID (null to remove parent)"}},"type":"object","title":"SetParentRequest","description":"Request model for setting device parent in hierarchy.\n\nRequirements:\n- 7.1: Set or remove device parent\n- 7.2: Validate parent_device_id\n- 7.7: Support null to remove parent\n\nFields:\n    parent_device_id: Parent device UUID (null to remove parent relationship)","examples":[{"description":"Set parent device","value":{"parent_device_id":"123e4567-e89b-12d3-a456-426614174000"}},{"description":"Remove parent (make device independent)","value":{}}]},"SubdomainUpdateRequest":{"properties":{"subdomain":{"type":"string","maxLength":63,"minLength":3,"pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$","title":"Subdomain","description":"Subdomain value (lowercase alphanumeric + hyphens, 3-63 chars, no leading/trailing hyphen)"}},"type":"object","required":["subdomain"],"title":"SubdomainUpdateRequest","description":"Subdomain update request.\n\nValidates subdomain format:\n- Lowercase alphanumeric and hyphens only\n- 3-63 characters\n- Cannot start or end with a hyphen","example":{"subdomain":"my-company"}},"SubscriptionTierUpdateRequest":{"properties":{"tier":{"type":"string","enum":["starter","growth"],"title":"Tier","description":"New subscription tier (founding tier not allowed via API)"}},"type":"object","required":["tier"],"title":"SubscriptionTierUpdateRequest","description":"Subscription tier update request.\n\nOnly allows upgrading/downgrading between publicly available tiers.\nFounding tier cannot be selected via API (manual assignment only).\n\nFields:\n    tier: New subscription tier (starter or growth only)","example":{"tier":"growth"}},"TeamMember":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"reseller_id":{"type":"string","format":"uuid","title":"Reseller Id"},"user_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"User Id"},"email":{"type":"string","title":"Email"},"status":{"type":"string","enum":["pending","active","removed"],"title":"Status"},"is_owner":{"type":"boolean","title":"Is Owner"},"invited_by":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Invited By"},"invited_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Invited At"},"joined_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Joined At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","reseller_id","email","status","is_owner","created_at"],"title":"TeamMember","description":"Team member model representing a user who can access a reseller account.\n\nMirrors TypeScript TeamMember interface from shared/types.ts\n\nFields:\n    id: Team member UUID\n    reseller_id: UUID of the reseller this member belongs to\n    user_id: UUID of the auth user (NULL for pending invites)\n    email: Team member email address\n    status: Member status (pending, active, removed)\n    is_owner: Whether this member is the account owner\n    invited_by: UUID of the user who sent the invite\n    invited_at: Timestamp when the invite was sent\n    joined_at: Timestamp when the member accepted the invite\n    created_at: Record creation timestamp","example":{"created_at":"2024-01-01T10:00:00+00:00","email":"team@example.com","id":"123e4567-e89b-12d3-a456-426614174000","invited_at":"2024-01-01T10:00:00+00:00","invited_by":"789e0123-e89b-12d3-a456-426614174002","is_owner":false,"joined_at":"2024-01-01T12:00:00+00:00","reseller_id":"789e0123-e89b-12d3-a456-426614174002","status":"active","user_id":"456e7890-e89b-12d3-a456-426614174001"}},"TeamMemberCreate":{"properties":{"email":{"type":"string","format":"email","title":"Email","description":"Email address of the person to invite"}},"type":"object","required":["email"],"title":"TeamMemberCreate","description":"Request model for inviting a new team member.\n\nUsed by POST /api/v1/reseller/team/invite endpoint.\n\nFields:\n    email: Email address of the person to invite","example":{"email":"colleague@company.com"}},"TeamMemberListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/TeamMember"},"type":"array","title":"Data"},"status":{"type":"string","const":"success","title":"Status","default":"success"}},"type":"object","required":["data"],"title":"TeamMemberListResponse","description":"Response wrapper for team member list operations.\n\nUsed by GET /api/v1/reseller/team endpoint.\n\nFields:\n    data: List of team members\n    status: Response status (always \"success\" for successful responses)","example":{"data":[{"created_at":"2024-01-01T00:00:00+00:00","email":"owner@company.com","id":"123e4567-e89b-12d3-a456-426614174000","is_owner":true,"joined_at":"2024-01-01T00:00:00+00:00","reseller_id":"789e0123-e89b-12d3-a456-426614174002","status":"active","user_id":"789e0123-e89b-12d3-a456-426614174002"},{"created_at":"2024-01-01T10:00:00+00:00","email":"colleague@company.com","id":"456e7890-e89b-12d3-a456-426614174001","invited_at":"2024-01-01T10:00:00+00:00","invited_by":"789e0123-e89b-12d3-a456-426614174002","is_owner":false,"joined_at":"2024-01-01T12:00:00+00:00","reseller_id":"789e0123-e89b-12d3-a456-426614174002","status":"active","user_id":"abc12345-e89b-12d3-a456-426614174003"}],"status":"success"}},"TeamMemberResponse":{"properties":{"data":{"$ref":"#/components/schemas/TeamMember"},"status":{"type":"string","const":"success","title":"Status","default":"success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["data"],"title":"TeamMemberResponse","description":"Response wrapper for single team member operations.\n\nUsed for invite, update, and get operations.\n\nFields:\n    data: The team member data\n    status: Response status (always \"success\" for successful responses)\n    message: Optional human-readable message","example":{"data":{"created_at":"2024-01-01T10:00:00+00:00","email":"colleague@company.com","id":"123e4567-e89b-12d3-a456-426614174000","invited_at":"2024-01-01T10:00:00+00:00","invited_by":"789e0123-e89b-12d3-a456-426614174002","is_owner":false,"reseller_id":"789e0123-e89b-12d3-a456-426614174002","status":"pending"},"message":"Invite sent successfully","status":"success"}},"ThresholdBulkUpdate":{"properties":{"device_ids":{"items":{"type":"string","format":"uuid"},"type":"array","maxItems":100,"minItems":1,"title":"Device Ids"},"enabled":{"type":"boolean","title":"Enabled"}},"type":"object","required":["device_ids","enabled"],"title":"ThresholdBulkUpdate","description":"Request payload for bulk threshold updates"},"ThresholdCreate":{"properties":{"daily_threshold_liters":{"type":"number","maximum":100000.0,"minimum":10.0,"title":"Daily Threshold Liters"},"enabled":{"type":"boolean","title":"Enabled","default":true}},"type":"object","required":["daily_threshold_liters"],"title":"ThresholdCreate","description":"Request payload for setting threshold"},"ToggleAddonRequest":{"properties":{"addon":{"type":"string","const":"custom_domain","title":"Addon"},"enabled":{"type":"boolean","title":"Enabled"}},"type":"object","required":["addon","enabled"],"title":"ToggleAddonRequest"},"TransferOwnershipRequest":{"properties":{"new_user_id":{"type":"string","format":"uuid","title":"New User Id","description":"UUID of the new owner"},"notes":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Notes","description":"Optional notes about the transfer"}},"type":"object","required":["new_user_id"],"title":"TransferOwnershipRequest","description":"Request model for transferring device ownership to a new user.\n\nThis is a formal ownership transfer that:\n- Generates a final consumption report for the current user\n- Records the transfer in ownership history\n- Sets the baseline reading for the new user\n\nRequirements:\n- Transfer device from current user to new user\n- Generate final report for current user\n- Record transfer in ownership history\n\nFields:\n    new_user_id: UUID of the new owner\n    notes: Optional notes about the transfer (reason, context)","examples":[{"description":"Transfer with notes","value":{"new_user_id":"123e4567-e89b-12d3-a456-426614174000","notes":"Tenant moved out on Jan 15, 2026. New tenant moved in same day."}},{"description":"Transfer without notes","value":{"new_user_id":"123e4567-e89b-12d3-a456-426614174000"}}]},"UpdateStatusPayload":{"properties":{"status":{"type":"string","enum":["pending","ignored"],"title":"Status","description":"Device status (pending or ignored)"}},"type":"object","required":["status"],"title":"UpdateStatusPayload","description":"Request payload for updating unregistered device status.\n\nUsed by PUT /api/v1/unregistered-devices/{id}/status endpoint\n\nFields:\n    status: Device status (pending or ignored)\n\nNote: 'registered' status can only be set by the system via registration endpoint","example":{"status":"ignored"}},"UserRedirectRequest":{"properties":{"user_id":{"type":"string","title":"User Id"},"email":{"type":"string","title":"Email"}},"type":"object","required":["user_id","email"],"title":"UserRedirectRequest","description":"Request body for getting user redirect URL"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyTokenRequest":{"properties":{"token_hash":{"type":"string","title":"Token Hash"},"type":{"type":"string","title":"Type","default":"magiclink"}},"type":"object","required":["token_hash"],"title":"VerifyTokenRequest","description":"Request body for server-side token verification"},"WebhookConfig":{"properties":{"webhook_url":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Webhook Url"},"webhook_enabled":{"type":"boolean","title":"Webhook Enabled","default":false}},"type":"object","title":"WebhookConfig","description":"Webhook configuration for alarm notifications"}}}}