{"openapi":"3.1.0","info":{"title":"SmartphoneKey B2C API","version":"1.0.0","description":"Event-sourced B2C API for SmartphoneKey smart lock management"},"servers":[{"url":"https://api.spkey.co","description":"Production"},{"url":"https://b2c-api.spk-stage.workers.dev","description":"Staging"},{"url":"https://b2c-api.spk-dev.workers.dev","description":"Development"},{"url":"http://localhost:8787","description":"Local"}],"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"JWT token from SmartphoneKey authentication. Identifies the B2C user or B2B service."},"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"API key for B2B organization access. Provided during organization onboarding."}},"schemas":{},"parameters":{}},"paths":{"/admin/threedeye/users/{userId}/reset":{"post":{"tags":["Admin","3dEye"],"summary":"Reset 3dEye provisioning for a user","description":"Clears the local threedeye_users/recovery rows (force re-provision) and optionally deletes the remote 3dEye account when visible in our customer. Requires dual auth: X-Admin-API-Key + JWT.","operationId":"post_ResetThreeDeyeUserRoute","parameters":[{"schema":{"type":"string","description":"b2c user UUID"},"required":true,"description":"b2c user UUID","name":"userId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"clearMapping":{"type":"boolean","default":true,"description":"Delete the local threedeye_users + recovery rows so the next fetch re-provisions."},"deleteRemote":{"type":"boolean","default":false,"description":"Also delete the remote 3dEye account if it is visible in our customer. Cannot remove accounts that live outside our customer."},"reprovision":{"type":"boolean","default":false,"description":"After clearing, immediately re-run provisioning instead of waiting for the next fetch."}}}}}},"responses":{"200":{"description":"Reset completed","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":["string","null"]},"clearedMapping":{"type":"boolean"},"clearedRecovery":{"type":"boolean"},"remote":{"type":"object","properties":{"attemptedDelete":{"type":"boolean"},"deletedThreedeyeUserId":{"type":["string","null"]},"notVisible":{"type":"boolean"}},"required":["attemptedDelete","deletedThreedeyeUserId","notVisible"]},"reprovision":{"type":"object","properties":{"attempted":{"type":"boolean"},"threedeyeUserId":{"type":["string","null"]},"error":{"type":["string","null"]}},"required":["attempted","threedeyeUserId","error"]}},"required":["userId","email","clearedMapping","clearedRecovery","remote","reprovision"]}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/admin/cleanup/hubs/{hubId}":{"delete":{"tags":["Admin","Cleanup"],"summary":"Delete hub aggregate","description":"Admin-only endpoint for deleting a hub aggregate. Supports two modes: DELETE_DO_ONLY removes the Durable Object instance while preserving R2 events (state restored on next access), and DELETE_DO_AND_EVENTS archives all events then permanently removes the DO instance. Requires dual authentication: X-Admin-API-Key header and a JWT with @smartphonekey.com email domain.","operationId":"delete_DeleteHubRoute","parameters":[{"schema":{"type":"string","description":"Hub ID (UUID)"},"required":true,"description":"Hub ID (UUID)","name":"hubId","in":"path"},{"schema":{"type":"string","enum":["DELETE_DO_ONLY","DELETE_DO_AND_EVENTS"],"description":"Deletion mode: DELETE_DO_ONLY preserves R2 events, DELETE_DO_AND_EVENTS archives and removes everything"},"required":true,"description":"Deletion mode: DELETE_DO_ONLY preserves R2 events, DELETE_DO_AND_EVENTS archives and removes everything","name":"mode","in":"query"}],"responses":{"200":{"description":"Hub deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"hubId":{"type":"string"},"mode":{"type":"string"},"operations":{"type":"array","items":{}},"deletionSummary":{"type":"string"},"auditLogPath":{"type":"string"},"duration":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["success","hubId","mode","operations","auditLogPath","duration","errors"]}}}},"400":{"description":"Invalid mode parameter","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}},"required":["success","error"]}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/admin/cleanup/locks/{lockId}":{"delete":{"tags":["Admin","Cleanup"],"summary":"Delete lock aggregate","description":"Admin-only endpoint for deleting a lock aggregate. Supports two modes: DELETE_DO_ONLY removes the Durable Object instance while preserving R2 events (state restored on next access), and DELETE_DO_AND_EVENTS archives all events then permanently removes the DO instance and D1 entries. The cascadeDeleteKeys parameter controls whether associated key_assignments are also removed. Requires dual authentication: X-Admin-API-Key header and a JWT with @smartphonekey.com email domain.","operationId":"delete_DeleteLockRoute","parameters":[{"schema":{"type":"string","description":"Lock ID (numeric) or UUID"},"required":true,"description":"Lock ID (numeric) or UUID","name":"lockId","in":"path"},{"schema":{"type":"string","enum":["DELETE_DO_ONLY","DELETE_DO_AND_EVENTS"],"description":"Deletion mode: DELETE_DO_ONLY preserves R2 events, DELETE_DO_AND_EVENTS archives and removes everything"},"required":true,"description":"Deletion mode: DELETE_DO_ONLY preserves R2 events, DELETE_DO_AND_EVENTS archives and removes everything","name":"mode","in":"query"},{"schema":{"type":"string","default":"true","description":"Whether to also delete associated key_assignments rows (default: true)"},"required":false,"description":"Whether to also delete associated key_assignments rows (default: true)","name":"cascadeDeleteKeys","in":"query"}],"responses":{"200":{"description":"Lock deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"lockId":{"type":"string"},"mode":{"type":"string"},"cascadeDeleteKeys":{"type":"boolean"},"operations":{"type":"array","items":{}},"deletionSummary":{"type":"string"},"auditLogPath":{"type":"string"},"duration":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["success","lockId","mode","cascadeDeleteKeys","operations","auditLogPath","duration","errors"]}}}},"400":{"description":"Invalid mode parameter","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}},"required":["success","error"]}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/admin/cleanup/users/{userId}":{"delete":{"tags":["Admin","Cleanup"],"summary":"Delete user aggregate","description":"Admin-only endpoint for deleting a user aggregate. Supports two modes: DELETE_DO_ONLY removes the Durable Object instance while preserving R2 events (the state will be restored on next access), and DELETE_DO_AND_EVENTS archives all events to a timestamped folder then permanently removes the DO instance and D1 entries. Requires dual authentication: X-Admin-API-Key header and a JWT with @smartphonekey.com email domain.","operationId":"delete_DeleteUserRoute","parameters":[{"schema":{"type":"string","description":"User ID to delete"},"required":true,"description":"User ID to delete","name":"userId","in":"path"},{"schema":{"type":"string","enum":["DELETE_DO_ONLY","DELETE_DO_AND_EVENTS"],"description":"Deletion mode: DELETE_DO_ONLY preserves R2 events, DELETE_DO_AND_EVENTS archives and removes everything"},"required":true,"description":"Deletion mode: DELETE_DO_ONLY preserves R2 events, DELETE_DO_AND_EVENTS archives and removes everything","name":"mode","in":"query"}],"responses":{"200":{"description":"User deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"userId":{"type":"string"},"mode":{"type":"string"},"operations":{"type":"array","items":{}},"deletionSummary":{"type":"string"},"auditLogPath":{"type":"string"},"duration":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["success","userId","mode","operations","auditLogPath","duration","errors"]}}}},"400":{"description":"Invalid mode parameter","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}},"required":["success","error"]}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/admin/threedeye/users/{userId}":{"get":{"tags":["Admin","3dEye"],"summary":"3dEye provisioning diagnostics for a user","description":"Read-only snapshot of a user's 3dEye provisioning state across D1 and 3dEye: the b2c user row, the threedeye_users mapping, any recovery row, and a live customer-scoped probe (user count + whether the email is visible). Requires dual auth: X-Admin-API-Key + JWT.","operationId":"get_GetThreeDeyeUserStatusRoute","parameters":[{"schema":{"type":"string","description":"b2c user UUID"},"required":true,"description":"b2c user UUID","name":"userId","in":"path"}],"responses":{"200":{"description":"3dEye diagnostics","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"user":{"type":"object","properties":{"found":{"type":"boolean"},"email":{"type":["string","null"]}},"required":["found","email"]},"mapping":{"type":"object","properties":{"found":{"type":"boolean"},"threedeyeUserId":{"type":["string","null"]},"hasPassword":{"type":"boolean"},"createdAt":{"type":["string","null"]},"remoteAccountExists":{"type":["boolean","null"]}},"required":["found","threedeyeUserId","hasPassword","createdAt","remoteAccountExists"]},"recovery":{"type":"object","properties":{"found":{"type":"boolean"},"roleIds":{"type":"array","items":{"type":"string"}}},"required":["found","roleIds"]},"remote":{"type":"object","properties":{"configured":{"type":"boolean"},"customerId":{"type":["string","null"]},"customerUserCount":{"type":["number","null"]},"emailPresentInCustomer":{"type":["boolean","null"]},"match":{"type":["object","null"],"properties":{"id":{"type":"string"},"email":{"type":"string"},"roleCount":{"type":"number"}},"required":["id","email","roleCount"]},"sampleEmails":{"type":"array","items":{"type":"string"}},"error":{"type":["string","null"]}},"required":["configured","customerId","customerUserCount","emailPresentInCustomer","match","sampleEmails","error"]}},"required":["userId","user","mapping","recovery","remote"]}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/locks/{id}/debug/messages":{"get":{"tags":["Lock","Query","Debug"],"summary":"Get MQTT debug messages","description":"Get captured MQTT messages for a lock (used for smoke tests)","operationId":"get_GetDebugMessagesRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"},{"schema":{"type":"string","default":"50","description":"Maximum number of messages to return"},"required":false,"description":"Maximum number of messages to return","name":"limit","in":"query"}],"responses":{"200":{"description":"MQTT debug messages retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"object","properties":{"command":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"payload":{},"timestamp":{"type":"string"}},"required":["id","timestamp"]}},"status":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"payload":{},"timestamp":{"type":"string"}},"required":["id","timestamp"]}}},"required":["command","status"]},"total":{"type":"number"}},"required":["messages","total"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/users/{id}/PhysicalKeys/{number}":{"get":{"tags":["User","Query","Physical Keys"],"summary":"Get a single physical key by number","description":"Returns the user's physical-key entry with the given number, or 404 if no such entry exists.","operationId":"get_GetPhysicalKeyRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"},{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Physical key number"},"required":true,"description":"Physical key number","name":"number","in":"path"},{"schema":{"type":"string","description":"Server-set: caller org id from API-key auth. Clients MUST NOT supply this."},"required":false,"description":"Server-set: caller org id from API-key auth. Clients MUST NOT supply this.","name":"_callerOrgId","in":"query"}],"responses":{"200":{"description":"Physical key retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"integer","exclusiveMinimum":0},"orgId":{"type":"string"},"orgName":{"type":"string"},"note":{"type":"string"},"addedAt":{"type":"number"}},"required":["number","orgId","addedAt"]}}}},"404":{"description":"User not found, or no physical key with that number","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/users/{id}/wallet/link":{"get":{"tags":["User","Query"],"summary":"Get wallet install URL","description":"Returns a URL the user can open to install their existing wallet key into Apple Wallet or Google Wallet. Returns 404 if no wallet key is set.","operationId":"get_GetUserWalletLinkRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"responses":{"200":{"description":"Wallet install URL retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"walletType":{"type":"string","enum":["APPLE","GOOGLE"]}},"required":["url","walletType"]}}}},"400":{"description":"User has wallet key but is missing required data (e.g. email for Apple)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}},"404":{"description":"User has no wallet key set","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/admin/cleanup/delete-all":{"post":{"tags":["Admin","Cleanup"],"summary":"Delete all aggregates (dev only)","description":"Admin-only endpoint that performs a complete environment wipe. Deletes all events from R2, removes all aggregates from D1 (users, locks, temp_keys), and cleans up Durable Object instances. API keys are preserved so subsequent tests still work. ONLY permitted in the dev environment — returns 403 Forbidden on stage or production. Requires dual authentication: X-Admin-API-Key header and a JWT with @smartphonekey.com email domain.","operationId":"post_DeleteAllRoute","responses":{"200":{"description":"All data deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"operations":{"type":"array","items":{}},"deletionSummary":{"type":"string"},"auditLogPath":{"type":"string"},"duration":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["success","operations","auditLogPath","duration","errors"]}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"403":{"description":"Not allowed on non-dev environment","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/admin/debug/registry":{"get":{"tags":["Admin","Debug"],"summary":"Get in-memory decorator registries","description":"Admin-only debug endpoint that returns the current state of the in-memory decorator registries (routes registered by @Route, event handlers registered by @EventHandler, and projectors registered via registerProjector). Useful for verifying decorator pickup at runtime — e.g. to confirm a refactor did not silently drop routes. Requires dual authentication (X-Admin-API-Key + JWT with @smartphonekey.com email domain), same as /admin/cleanup/*.","security":[{"adminApiKey":[],"bearerAuth":[]}],"operationId":"get_GetRegistryRoute","responses":{"200":{"description":"Current contents of the in-memory decorator registries","content":{"application/json":{"schema":{"type":"object","properties":{"routes":{"type":"array","items":{"type":"object","properties":{"method":{"type":"string"},"path":{"type":"string"},"className":{"type":"string"},"aggregateType":{"type":"string"},"isCreateCommand":{"type":"boolean"}},"required":["method","path","className"]}},"eventHandlers":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"eventType":{"type":"string"},"aggregateType":{"type":"string"},"className":{"type":"string"}},"required":["key","eventType","aggregateType","className"]}},"projectors":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"className":{"type":"string"}},"required":["id","className"]}},"summary":{"type":"object","properties":{"routeCount":{"type":"integer"},"eventHandlerCount":{"type":"integer"},"projectorCount":{"type":"integer"}},"required":["routeCount","eventHandlerCount","projectorCount"]}},"required":["routes","eventHandlers","projectors","summary"]}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"403":{"description":"Invalid email domain","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/admin/shadow-sync/{thingName}":{"post":{"tags":["Admin","Shadow"],"summary":"Force-sync every named shadow on a Thing through the EventBridge pipeline","description":"Enumerates every named shadow on the target Thing (via AWS IoT `ListNamedShadowsForThing` + `GetThingShadow`), then invokes the `shadow_to_eventbridge` Lambda once per shadow with a synthetic Topic-Rule-SQL-shaped event. Lambda → EventBridge → `/admin/shadow-webhook` → SpacetimeDB populates `device_named_shadow` as if the hub had organically republished. Auth: X-Admin-API-Key + @smartphonekey.com JWT.","operationId":"post_ShadowSyncRoute","parameters":[{"schema":{"type":"string","minLength":1,"description":"AWS IoT thing name to sync. For Matter devices this equals the device UUID."},"required":true,"description":"AWS IoT thing name to sync. For Matter devices this equals the device UUID.","name":"thingName","in":"path"}],"responses":{"200":{"description":"Shadow sync triggered for every named shadow on the Thing","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"thingName":{"type":"string"},"shadows":{"type":"array","items":{"type":"object","properties":{"shadowName":{"type":"string"},"lambdaStatusCode":{"type":["number","null"]},"lambdaRequestId":{"type":["string","null"]},"error":{"type":["string","null"]}},"required":["shadowName","lambdaStatusCode","lambdaRequestId","error"]}},"errors":{"type":"array","items":{"type":"string"}}},"required":["success","thingName","shadows","errors"]}}}},"400":{"description":"Invalid thingName","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}},"required":["success","error"]}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Thing has no named shadows","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/camera-roles/{id}/AssignCamera":{"post":{"tags":["CameraRole","Command"],"summary":"Assign camera to role","description":"Assigns a camera with permissions to the role in 3dEye and persists the assignment event.","operationId":"post_AssignCameraRoute","parameters":[{"schema":{"type":"string","description":"Camera Role ID"},"required":true,"description":"Camera Role ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"cameraId":{"type":["string","null"],"minLength":1,"description":"Camera ID in 3dEye (numeric, coerced to string)"},"permissions":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"description":"Permissions (e.g. View, Share)"}},"required":["permissions"]}}}},"responses":{"200":{"description":"Camera assigned to role successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraRoleCameraAssigned"]},"data":{"type":"object","properties":{"roleId":{"type":"string"},"cameraId":{"type":"string"},"permissions":{"type":"array","items":{"type":"string"}},"assignedAt":{"type":"number"}},"required":["roleId","cameraId","permissions","assignedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — camera role does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/camera-roles/{id}/AssignUser":{"post":{"tags":["CameraRole","Command"],"summary":"Assign user to camera role","description":"Assigns a user to the role in 3dEye and persists the assignment event.","operationId":"post_AssignUserRoute","parameters":[{"schema":{"type":"string","description":"Camera Role ID"},"required":true,"description":"Camera Role ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","description":"Our user UUID — server resolves 3dEye account on demand"}},"required":["userId"]}}}},"responses":{"200":{"description":"User assigned to camera role successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraRoleUserAssigned"]},"data":{"type":"object","properties":{"roleId":{"type":"string"},"userId":{"type":"string"},"threeDeyeUserId":{"type":"string"},"assignedAt":{"type":"number"}},"required":["roleId","userId","threeDeyeUserId","assignedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — camera role does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/camera-roles/{id}/CreateCameraRole":{"post":{"tags":["CameraRole","Command"],"summary":"Create camera role","description":"Creates a role in 3dEye and persists the creation event.","operationId":"post_CreateCameraRoleRoute","parameters":[{"schema":{"type":"string","description":"Camera Role ID"},"required":true,"description":"Camera Role ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"roleId":{"type":"string","description":"Camera Role ID"},"name":{"type":"string","minLength":1,"description":"Role name"},"description":{"type":"string","minLength":1,"description":"Role description"},"orgId":{"type":"string","minLength":1,"description":"Organization ID — opaque string, conventionally but not always a UUID"}},"required":["roleId","name","description","orgId"]}}}},"responses":{"200":{"description":"Camera role already exists — idempotent no-op (event: null)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraRoleCreated"]},"data":{"type":"object","properties":{"roleId":{"type":"string"},"threeDeyeRoleId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"orgId":{"type":"string"},"createdAt":{"type":"number"}},"required":["roleId","threeDeyeRoleId","name","description","orgId","createdAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"201":{"description":"Camera role created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraRoleCreated"]},"data":{"type":"object","properties":{"roleId":{"type":"string"},"threeDeyeRoleId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"orgId":{"type":"string"},"createdAt":{"type":"number"}},"required":["roleId","threeDeyeRoleId","name","description","orgId","createdAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"401":{"description":"Unauthorized — missing or invalid authentication credentials.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/camera-roles/{id}/DeleteCameraRole":{"post":{"tags":["CameraRole","Command"],"summary":"Delete camera role","description":"Deletes the role from 3dEye and persists the deletion event.","operationId":"post_DeleteCameraRoleRoute","parameters":[{"schema":{"type":"string","description":"Camera Role ID"},"required":true,"description":"Camera Role ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"deletedBy":{"type":"string","description":"User ID who deleted the role"}},"required":["deletedBy"]}}}},"responses":{"200":{"description":"Camera role deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraRoleDeleted"]},"data":{"type":"object","properties":{"roleId":{"type":"string"},"deletedAt":{"type":"number"}},"required":["roleId","deletedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — camera role does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/camera-roles/{id}/RemoveCamera":{"post":{"tags":["CameraRole","Command"],"summary":"Remove camera from role","description":"Removes a camera from the role in 3dEye and persists the removal event.","operationId":"post_RemoveCameraRoute","parameters":[{"schema":{"type":"string","description":"Camera Role ID"},"required":true,"description":"Camera Role ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"cameraId":{"type":["string","null"],"minLength":1,"description":"Camera ID in 3dEye (numeric, coerced to string)"}}}}}},"responses":{"200":{"description":"Camera removed from role successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraRoleCameraRemoved"]},"data":{"type":"object","properties":{"roleId":{"type":"string"},"cameraId":{"type":"string"},"removedAt":{"type":"number"}},"required":["roleId","cameraId","removedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — camera role does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/camera-roles/{id}/RemoveUser":{"post":{"tags":["CameraRole","Command"],"summary":"Remove user from camera role","description":"Removes a user from the role in 3dEye and persists the removal event.","operationId":"post_RemoveUserRoute","parameters":[{"schema":{"type":"string","description":"Camera Role ID"},"required":true,"description":"Camera Role ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","description":"Our user UUID — server resolves 3dEye account on demand"}},"required":["userId"]}}}},"responses":{"200":{"description":"User removed from camera role successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraRoleUserRemoved"]},"data":{"type":"object","properties":{"roleId":{"type":"string"},"userId":{"type":"string"},"threeDeyeUserId":{"type":"string"},"removedAt":{"type":"number"}},"required":["roleId","userId","threeDeyeUserId","removedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — camera role does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/camera-roles/{id}/details":{"get":{"tags":["CameraRole","Query"],"summary":"Get camera role details","description":"Get full camera role details including users and cameras arrays","operationId":"get_GetCameraRoleDetailsRoute","parameters":[{"schema":{"type":"string","description":"Camera Role ID"},"required":true,"description":"Camera Role ID","name":"id","in":"path"}],"responses":{"200":{"description":"Camera role details retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"roleId":{"type":"string"},"threeDeyeRoleId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"orgId":{"type":"string"},"status":{"type":"string","enum":["Active","Deleted"]},"users":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string"},"threeDeyeUserId":{"type":"string"}},"required":["userId","threeDeyeUserId"]}},"cameras":{"type":"array","items":{"type":"object","properties":{"cameraId":{"type":"string"},"permissions":{"type":"array","items":{"type":"string"}}},"required":["cameraId","permissions"]}},"version":{"type":"number"},"timestamp":{"type":"string"}},"required":["roleId","threeDeyeRoleId","name","description","orgId","status","users","cameras","version","timestamp"]}}}},"404":{"description":"Camera role not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/cameras/{id}/DeleteCamera":{"post":{"tags":["Camera","Command"],"summary":"Delete camera","description":"Deletes the camera from 3dEye and persists the deletion event.","operationId":"post_DeleteCameraRoute","parameters":[{"schema":{"type":"string","description":"Camera ID"},"required":true,"description":"Camera ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"deletedBy":{"type":"string","description":"User ID who deleted the camera"}},"required":["deletedBy"]}}}},"responses":{"200":{"description":"Camera deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraDeleted"]},"data":{"type":"object","properties":{"cameraId":{"type":"string"},"deletedBy":{"type":"string"},"deletedAt":{"type":"number"}},"required":["cameraId","deletedBy","deletedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — camera does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/cameras/{id}/DisableCamera":{"post":{"tags":["Camera","Command"],"summary":"Disable camera","description":"Soft-disables a camera without deleting it from 3dEye.","operationId":"post_DisableCameraRoute","parameters":[{"schema":{"type":"string","description":"Camera ID"},"required":true,"description":"Camera ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","minLength":1,"description":"Reason for disabling the camera"},"disabledBy":{"type":"string","description":"User ID who disabled the camera"}},"required":["reason","disabledBy"]}}}},"responses":{"200":{"description":"Camera disabled successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraDisabled"]},"data":{"type":"object","properties":{"cameraId":{"type":"string"},"reason":{"type":"string"},"disabledBy":{"type":"string"},"disabledAt":{"type":"number"}},"required":["cameraId","reason","disabledBy","disabledAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — camera does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/cameras/{id}/LinkCameraToLock":{"post":{"tags":["Camera","Command"],"summary":"Link camera to lock","description":"Links a camera to a lock for LPR automation (camera sees plate, lock opens).","operationId":"post_LinkCameraToLockRoute","parameters":[{"schema":{"type":"string","description":"Camera ID"},"required":true,"description":"Camera ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lockId":{"type":"string","description":"Lock ID to link this camera to"}},"required":["lockId"]}}}},"responses":{"200":{"description":"Camera linked to lock successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraLinkedToLock"]},"data":{"type":"object","properties":{"cameraId":{"type":"string"},"lockId":{"type":"string"}},"required":["cameraId","lockId"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — camera does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/cameras/{id}/RegisterCamera":{"post":{"tags":["Camera","Command"],"summary":"Register camera","description":"Registers a camera in 3dEye and persists the registration event.","operationId":"post_RegisterCameraRoute","parameters":[{"schema":{"type":"string","description":"Camera ID"},"required":true,"description":"Camera ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"cameraId":{"type":"string","description":"Camera ID"},"name":{"type":"string","minLength":1,"description":"Camera name"},"type":{"type":"string","enum":["generic","onvif","pnp"],"description":"Camera type"},"httpUrl":{"type":"string","format":"uri","description":"HTTP access URL for the camera (http:// or https://)"},"rtspUrl":{"type":"string","format":"uri","description":"RTSP streaming URL for the camera (rtsp://)"},"orgId":{"type":"string","minLength":1,"description":"Organization ID — opaque string, conventionally but not always a UUID"},"ownerId":{"type":"string","description":"Owner user ID"},"login":{"type":"string","minLength":1,"description":"Camera admin login name (required by 3dEye)"},"password":{"type":"string","minLength":1,"description":"Camera admin password (required by 3dEye)"},"port":{"type":"integer","exclusiveMinimum":0,"maximum":65535,"description":"Optional RTSP port — required for ONVIF cameras (parsed from rtspUrl if omitted)."}},"required":["cameraId","name","type","httpUrl","rtspUrl","orgId","ownerId","login","password"]}}}},"responses":{"200":{"description":"Camera already registered — idempotent no-op (event: null)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraRegistered"]},"data":{"type":"object","properties":{"cameraId":{"type":"string"},"threeDeyeCameraId":{"type":"string"},"cameraType":{"type":"string","enum":["generic","onvif","pnp"]},"name":{"type":"string"},"orgId":{"type":"string"},"ownerId":{"type":"string"},"httpUrl":{"type":"string"},"rtspUrl":{"type":"string"},"registrationCode":{"type":"string"},"registeredAt":{"type":"number"}},"required":["cameraId","threeDeyeCameraId","cameraType","name","orgId","ownerId","httpUrl","rtspUrl","registeredAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"201":{"description":"Camera registered successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraRegistered"]},"data":{"type":"object","properties":{"cameraId":{"type":"string"},"threeDeyeCameraId":{"type":"string"},"cameraType":{"type":"string","enum":["generic","onvif","pnp"]},"name":{"type":"string"},"orgId":{"type":"string"},"ownerId":{"type":"string"},"httpUrl":{"type":"string"},"rtspUrl":{"type":"string"},"registrationCode":{"type":"string"},"registeredAt":{"type":"number"}},"required":["cameraId","threeDeyeCameraId","cameraType","name","orgId","ownerId","httpUrl","rtspUrl","registeredAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated. login and password are required (3dEye requires AdminName/AdminPassword).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"401":{"description":"Unauthorized — missing or invalid authentication credentials.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/cameras/{id}/UnlinkCameraFromLock":{"post":{"tags":["Camera","Command"],"summary":"Unlink camera from lock","description":"Removes the lock association from a camera.","operationId":"post_UnlinkCameraFromLockRoute","parameters":[{"schema":{"type":"string","description":"Camera ID"},"required":true,"description":"Camera ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Camera unlinked from lock successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraUnlinkedFromLock"]},"data":{"type":"object","properties":{"cameraId":{"type":"string"}},"required":["cameraId"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — camera does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/cameras/{id}/UpdateCamera":{"post":{"tags":["Camera","Command"],"summary":"Update camera","description":"Updates camera details in 3dEye and persists the update event.","operationId":"post_UpdateCameraRoute","parameters":[{"schema":{"type":"string","description":"Camera ID"},"required":true,"description":"Camera ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Updated camera name"},"httpUrl":{"type":"string","format":"uri","description":"Updated HTTP access URL"},"rtspUrl":{"type":"string","format":"uri","description":"Updated RTSP streaming URL"}}}}}},"responses":{"200":{"description":"Camera updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["CameraUpdated"]},"data":{"type":"object","properties":{"cameraId":{"type":"string"},"name":{"type":"string"},"httpUrl":{"type":"string"},"rtspUrl":{"type":"string"},"updatedAt":{"type":"number"}},"required":["cameraId","updatedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — camera does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/cameras/{id}/details":{"get":{"tags":["Camera","Query"],"summary":"Get camera details","description":"Get full camera details including status and 3dEye identifiers","operationId":"get_GetCameraDetailsRoute","parameters":[{"schema":{"type":"string","description":"Camera ID"},"required":true,"description":"Camera ID","name":"id","in":"path"}],"responses":{"200":{"description":"Camera details retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"cameraId":{"type":"string"},"threeDeyeCameraId":{"type":"string"},"type":{"type":"string","enum":["generic","onvif","pnp"]},"name":{"type":"string"},"orgId":{"type":"string"},"ownerId":{"type":"string"},"status":{"type":"string","enum":["Active","Disabled","Deleted"]},"httpUrl":{"type":"string"},"rtspUrl":{"type":"string"},"registrationCode":{"type":"string"},"lockId":{"type":"string"},"version":{"type":"number"},"timestamp":{"type":"string"}},"required":["cameraId","threeDeyeCameraId","type","name","orgId","ownerId","status","httpUrl","rtspUrl","registrationCode","lockId","version","timestamp"]}}}},"404":{"description":"Camera not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/cameras/{id}/stream":{"get":{"tags":["Camera","Query"],"summary":"Get camera streaming URLs","description":"Fetches live streaming URLs (HLS, DASH, RTMP, thumbnail, snapshot) from 3dEye for a specific camera. Requires the authenticated user to have View permission on the camera.","operationId":"get_GetCameraStreamRoute","parameters":[{"schema":{"type":"string","description":"Camera ID"},"required":true,"description":"Camera ID","name":"id","in":"path"}],"responses":{"200":{"description":"Streaming URLs retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"cameraId":{"type":"string","description":"Camera ID"},"threeDeyeCameraId":{"type":"string","description":"3dEye camera ID"},"name":{"type":"string","description":"Camera display name"},"videoStreamState":{"type":"string","description":"Current video stream state from 3dEye"},"accessUrls":{"type":"object","properties":{"http":{"type":"string","description":"HTTP stream URL"},"rtsp":{"type":"string","description":"RTSP stream URL"},"hlsStream":{"type":"string","description":"HLS stream URL"},"dashStream":{"type":"string","description":"DASH stream URL"},"rtmpStream":{"type":"string","description":"RTMP stream URL"},"thumbnail":{"type":"string","description":"Thumbnail URL"},"snapShot":{"type":"string","description":"Snapshot URL"}},"required":["http","rtsp","hlsStream","dashStream","rtmpStream","thumbnail","snapShot"],"description":"Stream access URLs for different protocols"}},"required":["cameraId","threeDeyeCameraId","name","videoStreamState","accessUrls"]}}}},"401":{"description":"Unauthorized — JWT authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"403":{"description":"Forbidden — user does not have View permission on this camera","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Camera not found or not active, or not linked to 3dEye","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"503":{"description":"3dEye integration not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/cameras/{id}/lpr-events":{"get":{"tags":["Camera","Query"],"summary":"Get LPR event history","description":"Returns paginated license plate recognition audit history for a camera. Supports from/to date filters.","operationId":"get_GetLprEventsRoute","parameters":[{"schema":{"type":"string","description":"Camera ID"},"required":true,"description":"Camera ID","name":"id","in":"path"},{"schema":{"type":"string","description":"Inclusive ISO-8601 start timestamp"},"required":false,"description":"Inclusive ISO-8601 start timestamp","name":"from","in":"query"},{"schema":{"type":"string","description":"Inclusive ISO-8601 end timestamp"},"required":false,"description":"Inclusive ISO-8601 end timestamp","name":"to","in":"query"},{"schema":{"type":"string","description":"1-based page number"},"required":false,"description":"1-based page number","name":"page","in":"query"},{"schema":{"type":"string","description":"Events per page, maximum 100"},"required":false,"description":"Events per page, maximum 100","name":"pageSize","in":"query"}],"responses":{"200":{"description":"LPR event history retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"cameraId":{"type":"string"},"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Audit event id"},"cameraId":{"type":"string","description":"Camera aggregate id"},"lockId":{"type":["string","null"],"description":"Linked lock id, when known"},"plate":{"type":"string","description":"Detected license plate"},"confidence":{"type":"number","description":"3dEye detection confidence"},"matched":{"type":"boolean","description":"Whether the plate matched an authorized user"},"matchedUserId":{"type":["string","null"],"description":"Matched user id, when matched"},"actionTaken":{"type":"string","description":"Action taken by the LPR matcher"},"timestamp":{"type":"string","description":"ISO-8601 event timestamp"}},"required":["id","cameraId","lockId","plate","confidence","matched","matchedUserId","actionTaken","timestamp"]}},"page":{"type":"number"},"pageSize":{"type":"number"},"total":{"type":"number"},"hasMore":{"type":"boolean"}},"required":["cameraId","events","page","pageSize","total","hasMore"]}}}},"404":{"description":"Camera not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/hubs/{id}/ClaimHub":{"post":{"tags":["Hub","Command"],"summary":"Claim hub","description":"Claims a provisioned hub and binds it to Organization and Site","operationId":"post_ClaimHubRoute","parameters":[{"schema":{"type":"string","description":"Hub ID"},"required":true,"description":"Hub ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"hubId":{"type":"string","description":"Hub ID"},"orgId":{"type":"string","description":"Organization ID. Normal keys: bound from the authenticated key — a mismatching value is rejected (403); omit it to claim into your own org. Super keys only: may set any org for a cross-org claim."},"siteId":{"type":"string","description":"Site ID"},"userId":{"type":"string","description":"User ID claiming the hub"}},"required":["hubId","siteId","userId"]}}}},"responses":{"200":{"description":"Hub claimed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["HubClaimed"]},"data":{"type":"object","properties":{"hubId":{"type":"string"},"orgId":{"type":"string"},"siteId":{"type":"string"},"claimedBy":{"type":"string"},"claimedAt":{"type":"number"}},"required":["hubId","orgId","siteId","claimedBy","claimedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]},"example":{"success":true,"aggregateId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","version":2}}}},"400":{"description":"Invalid request — hub not provisioned or already claimed.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — the hub does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"502":{"description":"Hub offline — the hub is not currently connected to the network.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/hubs/{id}/CommissionDevice":{"post":{"tags":["Hub","Command"],"summary":"Commission device on hub","description":"Sends a commission_with_code command to the hub via AWS IoT Shadow. The hub performs Matter device commissioning and reports the result asynchronously.","operationId":"post_CommissionDeviceRoute","parameters":[{"schema":{"type":"string","description":"Hub ID"},"required":true,"description":"Hub ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"hubId":{"type":"string","description":"Hub ID"},"commissioningCode":{"type":"string","pattern":"^(\\d{11}|\\d{22}|MT:.+)$","description":"Matter commissioning code (11 or 22 digits, or MT:<code>)"}},"required":["hubId","commissioningCode"]}}}},"responses":{"200":{"description":"Commissioning command sent to hub successfully. The hub will report the result asynchronously.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":"null"}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — invalid commissioning code format or missing arguments.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — the hub does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"502":{"description":"Shadow update failed — could not reach AWS IoT.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/hubs/{id}/ProvisionHub":{"post":{"tags":["Hub","Command"],"summary":"Provision hub","description":"Registers a manufactured hub in the backend. The IoT Thing must already exist in AWS IoT (created during manufacturing).","operationId":"post_ProvisionHubRoute","parameters":[{"schema":{"type":"string","description":"Hub ID"},"required":true,"description":"Hub ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"hubId":{"type":"string","description":"Hub ID"},"serial":{"type":"string","minLength":1,"description":"Hub serial number"},"model":{"type":"string","minLength":1,"description":"Hub model identifier"},"thingName":{"type":"string","description":"AWS IoT Thing name (defaults to hubId)"},"vendorName":{"type":"string","description":"Vendor/manufacturer name"}},"required":["hubId","serial","model"]}}}},"responses":{"200":{"description":"Hub already provisioned — idempotent no-op (event: null)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["HubProvisioned"]},"data":{"type":"object","properties":{"hubId":{"type":"string"},"serial":{"type":"string"},"model":{"type":"string"},"thingName":{"type":"string"},"vendorName":{"type":"string"},"provisionedAt":{"type":"number"}},"required":["hubId","serial","model","thingName","provisionedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"201":{"description":"Hub provisioned successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["HubProvisioned"]},"data":{"type":"object","properties":{"hubId":{"type":"string"},"serial":{"type":"string"},"model":{"type":"string"},"thingName":{"type":"string"},"vendorName":{"type":"string"},"provisionedAt":{"type":"number"}},"required":["hubId","serial","model","thingName","provisionedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"401":{"description":"Unauthorized — missing or invalid authentication credentials.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/hubs/{id}/ReClaimHub":{"post":{"tags":["Hub","Command"],"summary":"Re-claim hub","description":"Re-claims a hub to a different Organization and Site","operationId":"post_ReClaimHubRoute","parameters":[{"schema":{"type":"string","description":"Hub ID"},"required":true,"description":"Hub ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"hubId":{"type":"string","description":"Hub ID"},"newOrgId":{"type":"string","description":"New Organization ID"},"newSiteId":{"type":"string","description":"New Site ID"},"reason":{"type":"string","minLength":1,"description":"Reason for re-claiming"},"userId":{"type":"string","description":"User ID performing re-claim"}},"required":["hubId","newOrgId","newSiteId","reason","userId"]}}}},"responses":{"200":{"description":"Hub re-claimed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["HubReClaimed"]},"data":{"type":"object","properties":{"hubId":{"type":"string"},"previousOrgId":{"type":"string"},"previousSiteId":{"type":"string"},"newOrgId":{"type":"string"},"newSiteId":{"type":"string"},"reclaimedBy":{"type":"string"},"reason":{"type":"string"},"reclaimedAt":{"type":"number"}},"required":["hubId","previousOrgId","previousSiteId","newOrgId","newSiteId","reclaimedBy","reason","reclaimedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — hub not claimed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — the hub does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"502":{"description":"Hub offline — the hub is not currently connected to the network.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/hubs/{id}/SendHubCommand":{"post":{"tags":["Hub","Command"],"summary":"Send hub command","description":"Sends hub-level commands (get_nodes, server_info, set_wifi_credentials)","operationId":"post_SendHubCommandRoute","parameters":[{"schema":{"type":"string","description":"Hub ID"},"required":true,"description":"Hub ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"hubId":{"type":"string","description":"Hub ID"},"command":{"type":"string","enum":["get_nodes","server_info","set_wifi_credentials"],"description":"Hub command type"},"args":{"type":"object","additionalProperties":{},"description":"Command arguments"}},"required":["hubId","command"]}}}},"responses":{"200":{"description":"Hub command sent successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":"null"}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — invalid command type or missing arguments.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — the hub does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"502":{"description":"Hub offline — the hub is not currently connected to the network.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/hubs/{id}/SetHubRole":{"post":{"tags":["Hub","Command"],"summary":"Set hub role","description":"Sets whether the hub is primary or secondary","operationId":"post_SetHubRoleRoute","parameters":[{"schema":{"type":"string","description":"Hub ID"},"required":true,"description":"Hub ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"hubId":{"type":"string","description":"Hub ID"},"isPrimary":{"type":"boolean","description":"Whether hub is primary"},"userId":{"type":"string","description":"User ID setting the role"}},"required":["hubId","isPrimary","userId"]}}}},"responses":{"200":{"description":"Hub role set successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["HubRoleSet"]},"data":{"type":"object","properties":{"hubId":{"type":"string"},"isPrimary":{"type":"boolean"},"setBy":{"type":"string"},"setAt":{"type":"number"}},"required":["hubId","isPrimary","setBy","setAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — hub not claimed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — the hub does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"502":{"description":"Hub offline — the hub is not currently connected to the network.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/hubs/{id}/SuspendHub":{"post":{"tags":["Hub","Command"],"summary":"Suspend hub","description":"Suspends a hub temporarily","operationId":"post_SuspendHubRoute","parameters":[{"schema":{"type":"string","description":"Hub ID"},"required":true,"description":"Hub ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"hubId":{"type":"string","description":"Hub ID"},"reason":{"type":"string","minLength":1,"description":"Reason for suspension"},"userId":{"type":"string","description":"User ID suspending the hub"}},"required":["hubId","reason","userId"]}}}},"responses":{"200":{"description":"Hub suspended successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["HubSuspended"]},"data":{"type":"object","properties":{"hubId":{"type":"string"},"reason":{"type":"string"},"suspendedBy":{"type":"string"},"suspendedAt":{"type":"number"}},"required":["hubId","reason","suspendedBy","suspendedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — hub already suspended or decommissioned.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — the hub does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"502":{"description":"Hub offline — the hub is not currently connected to the network.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/hubs/{id}/status":{"get":{"tags":["Hub","Query"],"summary":"Get hub status","description":"Get hub status including real-time online/offline from Thing Shadow","operationId":"get_GetHubStatusRoute","parameters":[{"schema":{"type":"string","description":"Hub ID"},"required":true,"description":"Hub ID","name":"id","in":"path"}],"responses":{"200":{"description":"Hub status retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"hubId":{"type":"string"},"serial":{"type":"string"},"model":{"type":"string"},"thingName":{"type":"string"},"status":{"type":"string","enum":["Provisioned","Claimed","Suspended","Decommissioned"]},"orgId":{"type":"string"},"siteId":{"type":"string"},"isPrimary":{"type":"boolean"},"provisionedAt":{"type":"number"},"claimedAt":{"type":"number"},"online":{"type":"boolean"},"lastSeen":{"type":"number"},"version":{"type":"string"},"uptimeSeconds":{"type":"number"}},"required":["hubId","serial","model","thingName","status","orgId","siteId","isPrimary","provisionedAt","claimedAt"]}}}},"404":{"description":"Hub not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/locks/{id}/AddKey":{"post":{"tags":["Lock","Command"],"summary":"Add key to lock","description":"Adds a key to the lock and sends command to device via AWS IoT Shadow","operationId":"post_AddKeyRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"keyUuid":{"type":"string","description":"Key identifier"},"keyIndexNumber":{"type":"integer","description":"Physical key index"},"userId":{"type":"string","description":"User ID"}},"required":["keyUuid"]}}}},"responses":{"200":{"description":"Key added successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["KeyAdded"]},"data":{"type":"object","properties":{"lockId":{"type":"number"},"uuid":{"type":"string"},"timestamp":{"type":"string"},"keyUuid":{"type":"string"},"keyIndexNumber":{"type":"number"},"userId":{"type":"string"}},"required":["lockId","uuid","timestamp","keyUuid"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"401":{"description":"Unauthorized — missing or invalid authentication credentials.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — the lock does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/locks/{id}/AddResident":{"post":{"tags":["Lock","Command","Doorbell"],"summary":"Add resident to doorbell list","description":"Adds a resident who will receive notifications when doorbell is activated. The resident must already exist in user_state (call GET /users/by-email first if not).","operationId":"post_AddResidentRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Resident email — server resolves to the user UUID via D1"},"name":{"type":"string","minLength":1,"description":"Resident name"}},"required":["email","name"]}}}},"responses":{"200":{"description":"Resident added successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["ResidentAdded"]},"data":{"type":"object","properties":{"lockId":{"type":"number"},"uuid":{"type":"string"},"timestamp":{"type":"string"},"userId":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}},"required":["lockId","uuid","timestamp","userId","name"]}},"required":["type","data"]},"email":{"type":"string"}},"required":["success","aggregateId","version","event","email"]}}}},"400":{"description":"Invalid request or resident already exists","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Lock not found, or no user exists for the supplied email — call GET /users/by-email first to provision.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/locks/{id}/ChangeLockUuid":{"post":{"tags":["Lock","Command"],"summary":"Change lock UUID (super API key only)","description":"Changes the hardware/device UUID of an existing lock. Restricted to super API keys only (SmartphoneKey internal use). Non-super API keys receive 403 Forbidden.","operationId":"post_ChangeLockUuidRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"newUuid":{"type":"string","description":"New UUID for the lock"}},"required":["newUuid"]}}}},"responses":{"200":{"description":"Lock UUID changed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["LockUuidChanged"]},"data":{"type":"object","properties":{"lockId":{"type":"number"},"uuid":{"type":"string"},"timestamp":{"type":"string"},"newUuid":{"type":"string"},"previousUuid":{"type":"string"}},"required":["lockId","uuid","timestamp","newUuid","previousUuid"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"401":{"description":"Unauthorized — missing or invalid authentication credentials.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"403":{"description":"Forbidden — requires super API key (SmartphoneKey internal only).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"409":{"description":"Conflict — UUID is the same as the current value.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/locks/{id}/CreateLock":{"post":{"tags":["Lock","Command"],"summary":"Create new lock","description":"Creates a new lock aggregate","operationId":"post_CreateLockRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"index":{"type":"integer","exclusiveMinimum":0,"description":"Lock index number"},"uuid":{"type":"string","description":"Lock UUID"},"serialNumber":{"type":"string","description":"Serial number"},"vendorName":{"type":"string","description":"Vendor/manufacturer name"},"ownerId":{"type":"string","description":"Owner user ID"},"keys":{"type":"array","items":{"type":"object","properties":{"uuid":{"type":"string"},"indexNumber":{"type":"integer"},"userId":{"type":"string"}},"required":["uuid"]},"default":[],"description":"Initial keys"}},"required":["index","uuid"]}}}},"responses":{"200":{"description":"Lock already exists — idempotent no-op (event: null)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["LockCreated"]},"data":{"type":"object","properties":{"lockId":{"type":"number"},"uuid":{"type":"string"},"serialNumber":{"type":"string"},"keys":{"type":"array","items":{"type":"object","properties":{"uuid":{"type":"string"},"indexNumber":{"type":"number"},"userId":{"type":"string"}},"required":["uuid"]}},"ownerId":{"type":"string"},"timestamp":{"type":"string"},"thingName":{"type":"string"},"thingArn":{"type":"string"}},"required":["lockId","uuid","timestamp"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"201":{"description":"Lock created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["LockCreated"]},"data":{"type":"object","properties":{"lockId":{"type":"number"},"uuid":{"type":"string"},"serialNumber":{"type":"string"},"keys":{"type":"array","items":{"type":"object","properties":{"uuid":{"type":"string"},"indexNumber":{"type":"number"},"userId":{"type":"string"}},"required":["uuid"]}},"ownerId":{"type":"string"},"timestamp":{"type":"string"},"thingName":{"type":"string"},"thingArn":{"type":"string"}},"required":["lockId","uuid","timestamp"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed or business rule violated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/locks/{id}/OpenLock":{"post":{"tags":["Lock","Command"],"summary":"Open lock","description":"Sends unlock command to device via AWS IoT Shadow. Returns 502 if shadow update fails.","operationId":"post_OpenLockRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","description":"Request tracking ID"},"reason":{"type":"string","description":"Reason for opening"}}}}}},"responses":{"200":{"description":"Lock opened successfully - command sent via AWS IoT Shadow","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["LockOpenRequested"]},"data":{"type":"object","properties":{"lockId":{"type":"number"},"uuid":{"type":"string"},"timestamp":{"type":"string"},"reason":{"type":"string"},"requestId":{"type":"string"}},"required":["lockId","uuid","timestamp"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden - insufficient permissions"},"404":{"description":"Lock not found"},"502":{"description":"Failed to update device shadow","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/locks/{id}/RemoveKey":{"post":{"tags":["Lock","Command"],"summary":"Remove key from lock","description":"Removes a key from the lock and sends command to device via AWS IoT Shadow","operationId":"post_RemoveKeyRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"keyUuid":{"type":"string","description":"Key identifier"},"keyIndexNumber":{"type":"integer","description":"Physical key index"}}}}}},"responses":{"200":{"description":"Key removed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["KeyRemoved"]},"data":{"type":"object","properties":{"lockId":{"type":"number"},"uuid":{"type":"string"},"timestamp":{"type":"string"},"keyUuid":{"type":"string"},"keyIndexNumber":{"type":"number"}},"required":["lockId","uuid","timestamp","keyUuid"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — missing key identifier (keyUuid or keyIndexNumber required).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"401":{"description":"Unauthorized — missing or invalid authentication credentials.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — the lock or key does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/locks/{id}/RemoveResident":{"post":{"tags":["Lock","Command","Doorbell"],"summary":"Remove resident from doorbell list","description":"Removes a resident from the doorbell notification list.","operationId":"post_RemoveResidentRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Resident email to remove"}},"required":["email"]}}}},"responses":{"200":{"description":"Resident removed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["ResidentRemoved"]},"data":{"type":"object","properties":{"lockId":{"type":"number"},"uuid":{"type":"string"},"timestamp":{"type":"string"},"userId":{"type":"string"},"email":{"type":"string"}},"required":["lockId","uuid","timestamp","userId"]}},"required":["type","data"]},"email":{"type":"string"}},"required":["success","aggregateId","version","event","email"]}}}},"400":{"description":"Invalid request or resident not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Lock not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/locks/{id}/SetOrganization":{"post":{"tags":["Lock","Command"],"summary":"Set lock organization","description":"Assigns an organization to a lock","operationId":"post_SetOrganizationRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"orgId":{"type":"string","description":"Organization ID"}},"required":["orgId"]}}}},"responses":{"200":{"description":"Organization set successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["OrganizationSet"]},"data":{"type":"object","properties":{"lockId":{"type":"number"},"uuid":{"type":"string"},"timestamp":{"type":"string"},"orgId":{"type":"string"}},"required":["lockId","uuid","timestamp","orgId"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"401":{"description":"Unauthorized — missing or invalid authentication credentials.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — the lock does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/locks/{id}/SetOwner":{"post":{"tags":["Lock","Command"],"summary":"Set lock owner","description":"Assigns a user as the owner of the lock","operationId":"post_SetOwnerRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","description":"User ID to set as owner"}},"required":["userId"]}}}},"responses":{"200":{"description":"Owner set successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["OwnerSet"]},"data":{"type":"object","properties":{"lockId":{"type":"number"},"uuid":{"type":"string"},"timestamp":{"type":"string"},"userId":{"type":"string"}},"required":["lockId","uuid","timestamp","userId"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"401":{"description":"Unauthorized — missing or invalid authentication credentials.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — the lock does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/locks/{id}/WriteKey":{"post":{"tags":["Lock","Command"],"summary":"Write key to lock firmware","description":"Sends write-key command to device via AWS IoT Shadow to write key data to physical lock hardware. Returns 502 if shadow update fails.","operationId":"post_WriteKeyRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"keyUuid":{"type":"string","minLength":1,"description":"Key identifier to write to device firmware"},"keyIndexNumber":{"type":"integer","description":"Physical key index on device"}},"required":["keyUuid"]}}}},"responses":{"200":{"description":"Key written to device successfully - command sent via AWS IoT Shadow","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["KeyWriteRequested"]},"data":{"type":"object","properties":{"lockId":{"type":"number"},"uuid":{"type":"string"},"timestamp":{"type":"string"},"keyUuid":{"type":"string"},"keyIndexNumber":{"type":"number"}},"required":["lockId","uuid","timestamp","keyUuid"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"403":{"description":"Forbidden - insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Lock not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"502":{"description":"Failed to update device shadow","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/locks/{id}/BulkAddKeys":{"post":{"tags":["Lock","Workflow"],"summary":"Bulk add keys to a lock via workflow","description":"Starts a Cloudflare Workflow that adds multiple keys to a lock. Keys are added sequentially with a 1-second delay between commands. Each key addition is a durable step that retries on failure.","operationId":"post_BulkAddKeysRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"object","properties":{"keyUuid":{"type":"string","description":"Key identifier"},"keyIndexNumber":{"type":"integer","description":"Physical key index"},"userId":{"type":"string","description":"User ID"}},"required":["keyUuid"]},"minItems":1,"description":"List of keys to add to the lock"}},"required":["keys"]}}}},"responses":{"200":{"description":"Workflow started successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"lockId":{"type":"number"},"instanceId":{"type":"string"}},"required":["success","lockId","instanceId"]}}}},"401":{"description":"Unauthorized — missing or invalid authentication credentials.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/locks/{id}/create-temp-key":{"post":{"tags":["Lock","TempKey"],"summary":"Create temporary web access key","description":"Creates a temporary web key for time-limited, shareable lock access. B2C requires lock owner, B2B requires org match.","operationId":"post_CreateTempKeyRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tempKeyUuid":{"type":"string"},"accessMode":{"type":"string","enum":["public","user-whitelist"]},"allowedUserIds":{"type":"array","items":{"type":"string"},"default":[]},"validFrom":{"type":"string","format":"date-time"},"validUntil":{"type":"string","format":"date-time"},"maxUses":{"type":["integer","null"],"exclusiveMinimum":0}},"required":["tempKeyUuid","accessMode","validFrom","validUntil"]}}}},"responses":{"200":{"description":"Temporary key created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["TempKeyCreated"]},"data":{"type":"object","properties":{"tempKeyUuid":{"type":"string"},"lockId":{"type":"number"},"lockUuid":{"type":"string"},"accessMode":{"type":"string","enum":["public","user-whitelist"]},"allowedUserIds":{"type":"array","items":{"type":"string"}},"validFrom":{"type":"string"},"validUntil":{"type":"string"},"maxUses":{"type":["number","null"]},"createdBy":{"type":"string"},"timestamp":{"type":"string"}},"required":["tempKeyUuid","lockId","lockUuid","accessMode","validFrom","validUntil","createdBy","timestamp"]}},"required":["type","data"]},"tempKeyUuid":{"type":"string"}},"required":["success","aggregateId","version","event","tempKeyUuid"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/locks/{id}/delete-temp-key":{"post":{"tags":["Lock","TempKey"],"summary":"Delete temporary web access key","description":"Deletes a temporary web key. B2C requires lock owner, B2B requires org match.","operationId":"post_DeleteTempKeyRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tempKeyUuid":{"type":"string"}},"required":["tempKeyUuid"]}}}},"responses":{"200":{"description":"Temporary key deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["TempKeyDeleted"]},"data":{"type":"object","properties":{"tempKeyUuid":{"type":"string"},"lockId":{"type":"number"},"lockUuid":{"type":"string"},"deletedBy":{"type":"string"},"timestamp":{"type":"string"}},"required":["tempKeyUuid","lockId","lockUuid","deletedBy","timestamp"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Temporary key not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/locks/{id}/basic-info":{"get":{"tags":["Lock","Query"],"summary":"Get basic lock info","description":"Get basic lock information without keys and residents","operationId":"get_GetBasicLockInfoRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"responses":{"200":{"description":"Basic lock info retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"lockId":{"type":"number"},"uuid":{"type":"string"},"version":{"type":"number"},"serialNumber":{"type":"string"},"ownerId":{"type":"string"},"orgId":{"type":"string"},"organizationId":{"type":"string"},"doId":{"type":"string","description":"Durable Object hex id for direct DO lookup and debugging"},"r2Prefix":{"type":"string","description":"R2 key prefix where this lock's events are stored"}},"required":["id","lockId","uuid","version","r2Prefix"]}}}},"404":{"description":"Lock not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/locks/{id}/details":{"get":{"tags":["Lock","Query"],"summary":"Get lock details","description":"Get full lock details including keys and residents","operationId":"get_GetLockDetailsRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"responses":{"200":{"description":"Lock details retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"lockId":{"type":"number"},"uuid":{"type":"string"},"serialNumber":{"type":"string"},"keys":{"type":"array","items":{"type":"object","properties":{"uuid":{"type":"string"},"indexNumber":{"type":"number"},"userId":{"type":"string"}},"required":["uuid"]}},"ownerId":{"type":"string"},"organizationId":{"type":"string"},"residents":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}},"required":["userId","name"]}},"orgId":{"type":"string"},"version":{"type":"number"},"timestamp":{"type":"string"},"doId":{"type":"string","description":"Durable Object hex id for direct DO lookup and debugging"},"r2Prefix":{"type":"string","description":"R2 key prefix where this lock's events are stored"}},"required":["id","lockId","uuid","keys","residents","orgId","version","timestamp","r2Prefix"]}}}},"404":{"description":"Lock not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/locks/{id}/events":{"get":{"tags":["Lock","Query"],"summary":"Get lock event history","description":"Returns paginated event history from R2. Default: last 10 events. Use ?page=N for older events.","operationId":"get_GetLockEventsRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"},{"schema":{"type":"string","description":"Page number (1-based). Default: last page (most recent events)"},"required":false,"description":"Page number (1-based). Default: last page (most recent events)","name":"page","in":"query"}],"responses":{"200":{"description":"Lock events retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"aggregateId":{"type":"string"},"totalEvents":{"type":"number"},"page":{"type":"number"},"pageSize":{"type":"number"},"totalPages":{"type":"number"},"events":{"type":"array","items":{"type":"object","properties":{"aggregateType":{"type":"string"},"aggregateId":{"type":"string"},"version":{"type":"number"},"type":{"type":"string"},"timestamp":{"type":"string"},"orgId":{"type":"string"},"event":{}},"required":["aggregateType","aggregateId","version","type","timestamp","orgId"]}}},"required":["aggregateId","totalEvents","page","pageSize","totalPages","events"]}}}},"404":{"description":"Lock not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/locks/{id}/residents":{"get":{"tags":["Lock","Query"],"summary":"Get residents list","description":"Get residents list for doorbell notifications (public endpoint)","operationId":"get_GetResidentsRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"responses":{"200":{"description":"Residents retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"residents":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}},"required":["userId","name"]}}},"required":["residents"]}}}},"404":{"description":"Lock not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/locks/{id}/temp-keys":{"get":{"tags":["Lock","TempKey"],"summary":"List temporary keys for lock","description":"Returns all temporary keys for the lock with computed status (valid, expired, used-up, not-yet-valid).","operationId":"get_ListTempKeysRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"responses":{"200":{"description":"Temporary keys retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"tempKeys":{"type":"array","items":{"type":"object","properties":{"uuid":{"type":"string"},"lockId":{"type":"number"},"lockUuid":{"type":"string"},"accessMode":{"type":"string","enum":["public","user-whitelist"]},"allowedUserIds":{"type":"array","items":{"type":"string"}},"validFrom":{"type":"string"},"validUntil":{"type":"string"},"maxUses":{"type":["number","null"]},"usedCount":{"type":"number"},"createdAt":{"type":"string"},"createdBy":{"type":"string"},"status":{"type":"string","enum":["valid","expired","used-up","not-yet-valid"]}},"required":["uuid","lockId","lockUuid","accessMode","allowedUserIds","validFrom","validUntil","maxUses","usedCount","createdAt","createdBy","status"]}}},"required":["tempKeys"]}}}},"404":{"description":"Lock not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/locks/{id}/update-temp-key":{"post":{"tags":["Lock","TempKey"],"summary":"Update temporary web access key","description":"Updates a temporary web key. Can modify dates, max uses, whitelist, and access mode. B2C requires lock owner, B2B requires org match.","operationId":"post_UpdateTempKeyRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tempKeyUuid":{"type":"string"},"validFrom":{"type":"string","format":"date-time"},"validUntil":{"type":"string","format":"date-time"},"maxUses":{"type":["integer","null"],"exclusiveMinimum":0},"allowedUserIds":{"type":"array","items":{"type":"string"}},"accessMode":{"type":"string","enum":["public","user-whitelist"]}},"required":["tempKeyUuid"]}}}},"responses":{"200":{"description":"Temporary key updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["TempKeyUpdated"]},"data":{"type":"object","properties":{"tempKeyUuid":{"type":"string"},"lockId":{"type":"number"},"lockUuid":{"type":"string"},"changes":{"type":"object","properties":{"validFrom":{"type":"string"},"validUntil":{"type":"string"},"maxUses":{"type":["number","null"]},"allowedUserIds":{"type":"array","items":{"type":"string"}},"accessMode":{"type":"string","enum":["public","user-whitelist"]}}},"updatedBy":{"type":"string"},"timestamp":{"type":"string"}},"required":["tempKeyUuid","lockId","lockUuid","changes","updatedBy","timestamp"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Temporary key not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/locks/{id}/use-temp-key":{"post":{"tags":["Lock","TempKey"],"summary":"Record temporary web access key usage","description":"Appends a TempKeyUsed audit event to the lock event stream (partner fan-out). Internal command issued by the temp-key open flow.","operationId":"post_UseTempKeyRoute","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Lock ID (numeric)"},"required":true,"description":"Lock ID (numeric)","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tempKeyUuid":{"type":"string"},"userId":{"type":"string"}},"required":["tempKeyUuid"]}}}},"responses":{"200":{"description":"Temporary key usage recorded successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["TempKeyUsed"]},"data":{"type":"object","properties":{"tempKeyUuid":{"type":"string"},"lockId":{"type":"number"},"lockUuid":{"type":"string"},"userId":{"type":"string"},"usedCount":{"type":"number"},"timestamp":{"type":"string"}},"required":["tempKeyUuid","lockId","lockUuid","usedCount","timestamp"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Lock not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/matter-devices/{id}/CreateMatterDevice":{"post":{"tags":["MatterDevice","Command"],"summary":"Create matter device","description":"Registers a new Matter device in the system","operationId":"post_CreateMatterDeviceRoute","parameters":[{"schema":{"type":"string","description":"Matter Device ID"},"required":true,"description":"Matter Device ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"deviceId":{"type":"string","description":"Matter Device ID"},"thingName":{"type":"string","description":"AWS IoT Thing name"},"hubId":{"type":"string","description":"Hub ID this device belongs to"},"deviceType":{"type":"string","default":"generic","description":"Matter device type"}},"required":["deviceId","thingName","hubId"]}}}},"responses":{"200":{"description":"Matter device already exists — idempotent no-op (event: null)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["MatterDeviceCreated"]},"data":{"type":"object","properties":{"deviceId":{"type":"string"},"thingName":{"type":"string"},"hubId":{"type":"string"},"deviceType":{"type":"string"},"createdAt":{"type":"number"}},"required":["deviceId","thingName","hubId","deviceType","createdAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"201":{"description":"Matter device created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["MatterDeviceCreated"]},"data":{"type":"object","properties":{"deviceId":{"type":"string"},"thingName":{"type":"string"},"hubId":{"type":"string"},"deviceType":{"type":"string"},"createdAt":{"type":"number"}},"required":["deviceId","thingName","hubId","deviceType","createdAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/matter-devices/{id}/UpdateReportedShadow":{"post":{"tags":["MatterDevice","Command"],"summary":"Update reported shadow","description":"Stores the reported shadow state received from the device via AWS IoT","operationId":"post_UpdateReportedShadowRoute","parameters":[{"schema":{"type":"string","description":"Matter Device ID"},"required":true,"description":"Matter Device ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reported":{"type":"object","additionalProperties":{},"description":"Reported shadow state from device"}},"required":["reported"]}}}},"responses":{"200":{"description":"Reported shadow stored","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["ReportedShadowUpdated"]},"data":{"type":"object","properties":{"deviceId":{"type":"string"},"thingName":{"type":"string"},"reported":{"type":"object","additionalProperties":{}},"updatedAt":{"type":"number"}},"required":["deviceId","thingName","reported","updatedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request or device not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/matter-devices/{id}/UpdateDesiredShadow":{"post":{"tags":["MatterDevice","Command"],"summary":"Update desired shadow","description":"Sets desired shadow state and pushes to AWS IoT Core","operationId":"post_UpdateDesiredShadowRoute","parameters":[{"schema":{"type":"string","description":"Matter Device ID"},"required":true,"description":"Matter Device ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"desired":{"type":"object","additionalProperties":{},"description":"Desired shadow state to push to device"}},"required":["desired"]}}}},"responses":{"200":{"description":"Desired shadow updated and pushed to device","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["DesiredShadowUpdated"]},"data":{"type":"object","properties":{"deviceId":{"type":"string"},"thingName":{"type":"string"},"desired":{"type":"object","additionalProperties":{}},"updatedAt":{"type":"number"}},"required":["deviceId","thingName","desired","updatedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request or device not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/rtsp-cameras/{id}/DeleteRtspCamera":{"post":{"tags":["RtspCamera","Command"],"summary":"Delete an RTSP camera","description":"Soft-deletes an RTSP camera aggregate (status transitions to Deleted)","operationId":"post_DeleteRtspCameraRoute","parameters":[{"schema":{"type":"string","description":"RTSP Camera UUID"},"required":true,"description":"RTSP Camera UUID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"deletedBy":{"type":"string","description":"Actor who requested the deletion"}}}}}},"responses":{"200":{"description":"RTSP camera deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["RtspCameraDeleted"]},"data":{"type":"object","properties":{"rtspCameraId":{"type":"string"},"deletedAt":{"type":"string"},"deletedBy":{"type":"string"}},"required":["rtspCameraId","deletedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Camera is already deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"401":{"description":"Unauthorized — missing or invalid authentication credentials.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"403":{"description":"Forbidden — you do not own this camera.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"404":{"description":"Not found — the camera does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/rtsp-cameras/{id}/RegisterRtspCamera":{"post":{"tags":["RtspCamera","Command"],"summary":"Register a new RTSP camera","description":"Creates a new RTSP camera aggregate for a simple plug-and-play camera","operationId":"post_RegisterRtspCameraRoute","parameters":[{"schema":{"type":"string","description":"RTSP Camera UUID"},"required":true,"description":"RTSP Camera UUID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"rtspCameraId":{"type":"string","description":"Camera UUID (caller-supplied or server-generated)"},"name":{"type":"string","minLength":1,"description":"Human-readable camera name"},"rtspUrl":{"type":"string","minLength":1,"description":"RTSP stream URL (e.g. rtsp://192.168.1.1:554/stream)"},"httpSnapshotUrl":{"type":"string","description":"HTTP snapshot URL for still image capture"},"username":{"type":"string","description":"Camera authentication username"},"password":{"type":"string","description":"Camera authentication password"},"ownerId":{"type":"string","description":"User ID who owns this camera (B2C)"},"orgId":{"type":"string","description":"Organisation ID that owns this camera (B2B)"}},"required":["rtspCameraId","name","rtspUrl"]}}}},"responses":{"200":{"description":"RTSP camera already registered — idempotent no-op (event: null)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["RtspCameraRegistered"]},"data":{"type":"object","properties":{"rtspCameraId":{"type":"string"},"name":{"type":"string"},"rtspUrl":{"type":"string"},"httpSnapshotUrl":{"type":"string"},"username":{"type":"string"},"ownerId":{"type":"string"},"orgId":{"type":"string"},"createdAt":{"type":"string"}},"required":["rtspCameraId","name","rtspUrl","createdAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"201":{"description":"RTSP camera registered successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["RtspCameraRegistered"]},"data":{"type":"object","properties":{"rtspCameraId":{"type":"string"},"name":{"type":"string"},"rtspUrl":{"type":"string"},"httpSnapshotUrl":{"type":"string"},"username":{"type":"string"},"ownerId":{"type":"string"},"orgId":{"type":"string"},"createdAt":{"type":"string"}},"required":["rtspCameraId","name","rtspUrl","createdAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request — validation failed.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/temp-keys/{uuid}/open":{"post":{"tags":["TempKey","Web"],"summary":"Open lock via temp key","description":"Validates a temporary key (expiration, usage limits, authorization) and sends an OpenLock command to the LockAggregate Durable Object. On success, increments the usage counter and redirects back to the temp key page. Public keys need no auth; user-whitelist keys require a JWT with a whitelisted userId.","operationId":"post_OpenTempKeyRoute","parameters":[{"schema":{"type":"string","description":"Temporary key UUID"},"required":true,"description":"Temporary key UUID","name":"uuid","in":"path"}],"responses":{"303":{"description":"Lock opened successfully, redirecting to temp key page"},"400":{"description":"Temporary key is invalid (not yet valid, expired, or used up)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"403":{"description":"User not authorized to use this temporary key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"404":{"description":"Temporary key not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/temp-keys/{uuid}/confirmation":{"get":{"tags":["TempKey","Web"],"summary":"Render temp key confirmation page","description":"Returns an HTML page confirming the door open command was sent. Displays lock info and a link back to the temp key page.","operationId":"get_TempKeyConfirmationRoute","parameters":[{"schema":{"type":"string","description":"Temporary key UUID"},"required":true,"description":"Temporary key UUID","name":"uuid","in":"path"},{"schema":{"type":["boolean","null"],"description":"Device was offline when command was sent"},"required":false,"description":"Device was offline when command was sent","name":"offline","in":"query"}],"responses":{"200":{"description":"Confirmation page rendered successfully","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"Temporary key not found","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/users/{id}/3deye-credentials":{"get":{"tags":["User","3dEye"],"summary":"Get 3dEye credentials for a user","description":"Returns the stored 3dEye user GUID and password for the b2c user. Lazy-creates the 3dEye account on first call. Auth: JWT or B2B X-API-Key, same as the rest of the user-scoped routes.","operationId":"get_GetThreeDeyeCredentialsRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"responses":{"200":{"description":"3dEye credentials found (or just provisioned)","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string"},"threeDeyeUserId":{"type":["string","null"]},"password":{"type":"string"}},"required":["userId","email","threeDeyeUserId","password"]}}}},"401":{"description":"Unauthorized — missing or invalid auth","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"No b2c user found for this id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"503":{"description":"3dEye not configured for this environment","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/users/{id}/AddAccessibleLock":{"post":{"tags":["User","Command","Lock Access"],"summary":"Grant lock access to user","description":"Grants a user access to a lock with specified access level (OWNER, ADMIN, USER, GUEST).","operationId":"post_AddAccessibleLockRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lockId":{"type":"string","description":"Lock ID"},"lockName":{"type":"string","description":"Lock name"},"accessLevel":{"type":"string","enum":["OWNER","ADMIN","USER","GUEST"],"description":"Access level"},"validFrom":{"type":"string","description":"Valid from date"},"validUntil":{"type":"string","description":"Valid until date"},"grantedBy":{"type":"string","description":"User who granted access"}},"required":["lockId","lockName","accessLevel","grantedBy"]}}}},"responses":{"200":{"description":"Lock access granted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["AccessibleLockAdded"]},"data":{"type":"object","properties":{"userId":{"type":"string"},"lockId":{"type":"string"},"lockName":{"type":"string"},"accessLevel":{"type":"string","enum":["OWNER","ADMIN","USER","GUEST"]},"grantedBy":{"type":"string"}},"required":["userId","lockId","lockName","accessLevel","grantedBy"]}},"required":["type","data"]},"lockId":{"type":"string"},"accessLevel":{"type":"string","enum":["OWNER","ADMIN","USER","GUEST"]}},"required":["success","aggregateId","version","event","lockId","accessLevel"]}}}},"400":{"description":"Invalid request or lock already accessible","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/users/{id}/AddDevice":{"post":{"tags":["User","Command","Devices"],"summary":"Link a device to a user","description":"Records a cross-reference between a user and a device aggregate (Matter bridge or 3dEye camera) along with the device's org.","operationId":"post_AddDeviceRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"deviceId":{"type":"string","description":"Device UUID"},"deviceType":{"type":"string","enum":["matter","camera","rtsp-camera"],"description":"matter | camera"},"orgId":{"type":"string","description":"Organisation the device belongs to"},"name":{"type":"string","description":"Friendly label"},"addedBy":{"type":"string","description":"Actor that performed the link"}},"required":["deviceId","deviceType","orgId"]}}}},"responses":{"200":{"description":"Device linked successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["DeviceAdded"]},"data":{"type":"object","properties":{"userId":{"type":"string"},"deviceId":{"type":"string"},"deviceType":{"type":"string","enum":["matter","camera","rtsp-camera"]},"orgId":{"type":"string"},"name":{"type":"string"},"addedBy":{"type":"string"},"addedAt":{"type":"string"}},"required":["userId","deviceId","deviceType","orgId","addedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request or device already linked","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/users/{id}/AddLicensePlate":{"post":{"tags":["User","Command","License Plate"],"summary":"Add license plate to user","description":"Adds a license plate to the user profile for LPR automation. Plates are normalized (uppercase, no spaces/dashes) and deduplicated.","operationId":"post_AddLicensePlateRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"plate":{"type":"string","minLength":1,"description":"License plate number"},"label":{"type":"string","default":"","description":"Human-readable label for the plate"}},"required":["plate"]}}}},"responses":{"200":{"description":"License plate added successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["LicensePlateAdded"]},"data":{"type":"object","properties":{"userId":{"type":"string"},"plate":{"type":"string"},"label":{"type":"string"},"addedAt":{"type":"number"}},"required":["userId","plate","label","addedAt"]}},"required":["type","data"]},"plate":{"type":"string"},"label":{"type":"string"}},"required":["success","aggregateId","version","event","plate","label"]}}}},"400":{"description":"Invalid request or plate already exists","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/users/{id}/AddPhysicalKey":{"post":{"tags":["User","Command","Physical Keys"],"summary":"Add a physical key to the user inventory","description":"Records that a physical key has been issued to the user by some organisation. Server assigns a per-user autoincrement `number` (returned in the response) which is never reused after removal.","operationId":"post_AddPhysicalKeyRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"orgId":{"type":"string","minLength":1,"description":"Issuing organisation id"},"orgName":{"type":"string","description":"Issuing organisation display name"},"note":{"type":"string","maxLength":500,"description":"Free-text note about the key (max 500 chars)"}},"required":["orgId"]}}}},"responses":{"200":{"description":"Physical key added successfully. Response includes the server-assigned number.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["PhysicalKeyAdded"]},"data":{"type":"object","properties":{"userId":{"type":"string"},"number":{"type":"number"},"orgId":{"type":"string"},"orgName":{"type":"string"},"note":{"type":"string"},"addedAt":{"type":"number"}},"required":["userId","number","orgId","addedAt"]}},"required":["type","data"]},"number":{"type":"integer","exclusiveMinimum":0,"description":"Server-assigned per-user key number"},"orgId":{"type":"string"},"orgName":{"type":"string"},"note":{"type":"string"},"addedAt":{"type":"number"}},"required":["success","aggregateId","version","event","number","orgId","addedAt"]}}}},"400":{"description":"Invalid request (e.g. missing orgId, note too long)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/users/{id}/AddWalletKey":{"post":{"tags":["User","Command","Wallet"],"summary":"Add wallet key to user","description":"Prepares a wallet key for a user (Apple Wallet or Google Wallet). The server generates the wallet keyId (UUID) inside the command handler and returns it in the response; clients must not send keyId in the request body.","operationId":"post_AddWalletKeyRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"walletType":{"type":"string","enum":["APPLE","GOOGLE"],"description":"Wallet type (APPLE or GOOGLE)"},"keyIndex":{"type":"number","description":"Key index"}},"required":["walletType","keyIndex"]}}}},"responses":{"200":{"description":"Wallet key prepared successfully. Response includes the server-generated keyId.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["WalletKeyPrepared"]},"data":{"type":"object","properties":{"userId":{"type":"string"},"keyId":{"type":"string"},"walletType":{"type":"string","enum":["APPLE","GOOGLE"]},"keyIndex":{"type":"number"},"deviceId":{"type":"string"}},"required":["userId","keyId","walletType"]}},"required":["type","data"]},"keyId":{"type":"string","description":"Server-generated wallet key UUID"},"walletType":{"type":"string","enum":["APPLE","GOOGLE"]}},"required":["success","aggregateId","version","event","keyId","walletType"]},"example":{"success":true,"aggregateId":"550e8400-e29b-41d4-a716-446655440000","version":2,"keyId":"00000000-0000-0000-0000-000000000000","walletType":"APPLE"}}}},"400":{"description":"Invalid request or wallet already provisioned","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/users/{id}/CreateUser":{"post":{"tags":["User","Command"],"summary":"Create new user","description":"Creates a new user aggregate with initial profile data.","operationId":"post_CreateUserRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","description":"User UUID"},"email":{"type":"string","format":"email","description":"User email"},"firstName":{"type":"string","description":"First name"},"lastName":{"type":"string","description":"Last name"},"phoneNumber":{"type":"string","description":"Phone number"}},"required":["userId","email"]}}}},"responses":{"200":{"description":"User already exists — idempotent no-op (event: null)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["UserCreated"]},"data":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","SUSPENDED"]}},"required":["userId","email","status"]}},"required":["type","data"]},"userId":{"type":"string"}},"required":["success","aggregateId","version","event","userId"]},"example":{"success":true,"aggregateId":"550e8400-e29b-41d4-a716-446655440000","version":1,"event":null}}}},"201":{"description":"User created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["UserCreated"]},"data":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","SUSPENDED"]}},"required":["userId","email","status"]}},"required":["type","data"]},"userId":{"type":"string"}},"required":["success","aggregateId","version","event","userId"]},"example":{"success":true,"aggregateId":"550e8400-e29b-41d4-a716-446655440000","version":1,"userId":"550e8400-e29b-41d4-a716-446655440000"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/users/{id}/accessible-locks":{"get":{"tags":["User","Query"],"summary":"Get accessible locks","description":"Get locks accessible by the user","operationId":"get_GetAccessibleLocksRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"responses":{"200":{"description":"Accessible locks retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"locks":{"type":"array","items":{"type":"object","properties":{"lockId":{"type":"string"},"lockName":{"type":"string"},"accessLevel":{"type":"string","enum":["OWNER","ADMIN","USER","GUEST"]},"grantedBy":{"type":"string"},"expiresAt":{"type":"string"}},"required":["lockId","lockName","accessLevel","grantedBy"]}},"totalCount":{"type":"number"}},"required":["locks","totalCount"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/users/{id}/license-plates":{"get":{"tags":["User","Query","License Plate"],"summary":"Get license plates","description":"Get the user's registered license plates for LPR automation","operationId":"get_GetLicensePlatesRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"responses":{"200":{"description":"License plates retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"licensePlates":{"type":"array","items":{"type":"object","properties":{"plate":{"type":"string"},"label":{"type":"string"},"addedAt":{"type":"number"}},"required":["plate","label","addedAt"]}},"totalCount":{"type":"number"}},"required":["licensePlates","totalCount"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/users/{id}/owned-locks":{"get":{"tags":["User","Query"],"summary":"Get locks owned by the user","description":"Returns locks where `LockAggregate.state.ownerId === user.userId`, sourced from the SpacetimeDB `device_state` read model.","operationId":"get_GetOwnedLocksRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"responses":{"200":{"description":"Owned locks retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"locks":{"type":"array","items":{"type":"object","properties":{"lockUuid":{"type":"string","description":"Lock device UUID"},"serialNumber":{"type":["string","null"],"description":"Manufacturer serial number"},"thingName":{"type":["string","null"],"description":"AWS IoT thing name"},"online":{"type":"boolean","description":"Whether the device is currently reporting online"},"keysCount":{"type":"integer","description":"Number of physical keys on the lock"},"residentCount":{"type":"integer","description":"Number of users granted access"},"lastEvent":{"type":["string","null"],"description":"Last projected event type"},"lastUpdated":{"type":["number","null"],"description":"Unix epoch ms when the last projected event was applied"}},"required":["lockUuid","serialNumber","thingName","online","keysCount","residentCount","lastEvent","lastUpdated"]}},"totalCount":{"type":"integer"},"source":{"type":"string","enum":["spacetimedb","unavailable"]}},"required":["locks","totalCount","source"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/users/{id}/devices":{"get":{"tags":["User","Query","Devices"],"summary":"Get devices linked to a user","description":"Returns the user's linked devices. Optional `?type=` query param filters by device kind. API-key (B2B) requests are auto-scoped to the key's orgId; JWT (B2C) callers see all of the user's devices.","operationId":"get_GetUserDevicesRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"},{"schema":{"type":"string","enum":["matter","camera","rtsp-camera"],"description":"Filter by device type (matter | camera)"},"required":false,"description":"Filter by device type (matter | camera)","name":"type","in":"query"}],"responses":{"200":{"description":"Devices retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"devices":{"type":"array","items":{"type":"object","properties":{"deviceId":{"type":"string"},"deviceType":{"type":"string","enum":["matter","camera","rtsp-camera"]},"orgId":{"type":"string"},"name":{"type":"string"},"addedBy":{"type":"string"},"addedAt":{"type":"string"}},"required":["deviceId","deviceType","orgId","addedAt"]}},"totalCount":{"type":"number"}},"required":["devices","totalCount"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/users/{id}/keys":{"get":{"tags":["User","Query"],"summary":"Get user keys","description":"Get user's physical and digital keys","operationId":"get_GetUserKeysRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"responses":{"200":{"description":"User keys retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"physicalKeys":{"type":"array","items":{"type":"object","properties":{"number":{"type":"integer","exclusiveMinimum":0},"orgId":{"type":"string"},"orgName":{"type":"string"},"note":{"type":"string"},"addedAt":{"type":"number"}},"required":["number","orgId","addedAt"]}},"digitalKeys":{"type":"array","items":{"type":"object","properties":{"keyId":{"type":"string"},"keyIndex":{"type":"number"},"walletType":{"type":"string","enum":["APPLE","GOOGLE"]},"deviceId":{"type":"string"},"addedAt":{"type":"string"}},"required":["keyId","keyIndex","walletType","deviceId","addedAt"]}},"walletKey":{"type":"object","properties":{"keyId":{"type":"string"},"keyIndex":{"type":"number"},"walletType":{"type":"string","enum":["APPLE","GOOGLE"]},"deviceId":{"type":"string"},"passUrl":{"type":"string"},"serialNumber":{"type":"string"},"addedAt":{"type":"string"}},"required":["keyId","keyIndex","walletType","deviceId","addedAt"]}},"required":["physicalKeys","digitalKeys"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/users/{id}/profile":{"get":{"tags":["User","Query"],"summary":"Get user profile","description":"Get user's profile information (name, email, status)","operationId":"get_GetUserProfileRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"responses":{"200":{"description":"User profile retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","SUSPENDED"]}},"required":["userId"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/users/{id}/wallet":{"get":{"tags":["User","Query"],"summary":"Get user wallet status","description":"Get user's wallet status and wallet key details","operationId":"get_GetUserWalletRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"responses":{"200":{"description":"User wallet status retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"hasWalletKey":{"type":"boolean"},"walletKey":{"type":"object","properties":{"keyId":{"type":"string"},"keyIndex":{"type":"number"},"walletType":{"type":"string","enum":["APPLE","GOOGLE"]},"deviceId":{"type":"string"},"passUrl":{"type":"string"},"serialNumber":{"type":"string"},"addedAt":{"type":"string"}},"required":["keyId","keyIndex","walletType","deviceId","addedAt"]}},"required":["hasWalletKey"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/users/{id}/RemoveAccessibleLock":{"post":{"tags":["User","Command","Lock Access"],"summary":"Revoke lock access from user","description":"Revokes a user's access to a lock.","operationId":"post_RemoveAccessibleLockRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lockId":{"type":"string","description":"Lock ID"},"removedBy":{"type":"string","description":"User who removed access"},"reason":{"type":"string","description":"Reason for removal"}},"required":["lockId","removedBy","reason"]}}}},"responses":{"200":{"description":"Lock access revoked successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["AccessibleLockRemoved"]},"data":{"type":"object","properties":{"userId":{"type":"string"},"lockId":{"type":"string"},"removedBy":{"type":"string"},"reason":{"type":"string"},"removedAt":{"type":"string"}},"required":["userId","lockId","removedBy","reason","removedAt"]}},"required":["type","data"]},"lockId":{"type":"string"},"removedAt":{"type":"string"}},"required":["success","aggregateId","version","event","lockId","removedAt"]}}}},"400":{"description":"Invalid request or lock not accessible","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/users/{id}/RemoveDevice":{"post":{"tags":["User","Command","Devices"],"summary":"Unlink a device from a user","description":"Removes the cross-reference between a user and a device aggregate. The device aggregate itself is not touched.","operationId":"post_RemoveDeviceRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"deviceId":{"type":"string","description":"Device UUID"},"removedBy":{"type":"string","description":"Actor that performed the unlink"},"reason":{"type":"string","description":"Why the device is being unlinked"}},"required":["deviceId","removedBy","reason"]}}}},"responses":{"200":{"description":"Device unlinked successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["DeviceRemoved"]},"data":{"type":"object","properties":{"userId":{"type":"string"},"deviceId":{"type":"string"},"deviceType":{"type":"string","enum":["matter","camera","rtsp-camera"]},"removedBy":{"type":"string"},"reason":{"type":"string"},"removedAt":{"type":"string"}},"required":["userId","deviceId","deviceType","removedBy","reason","removedAt"]}},"required":["type","data"]}},"required":["success","aggregateId","version","event"]}}}},"400":{"description":"Invalid request or device not linked","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/users/{id}/RemoveLicensePlate":{"post":{"tags":["User","Command","License Plate"],"summary":"Remove license plate from user","description":"Removes a license plate from the user's profile.","operationId":"post_RemoveLicensePlateRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"plate":{"type":"string","minLength":1,"description":"License plate number to remove"}},"required":["plate"]}}}},"responses":{"200":{"description":"License plate removed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["LicensePlateRemoved"]},"data":{"type":"object","properties":{"userId":{"type":"string"},"plate":{"type":"string"}},"required":["userId","plate"]}},"required":["type","data"]},"plate":{"type":"string"}},"required":["success","aggregateId","version","event","plate"]}}}},"400":{"description":"Invalid request or plate not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/users/{id}/RemovePhysicalKey":{"post":{"tags":["User","Command","Physical Keys"],"summary":"Remove a physical key from the user inventory","description":"Removes the physical-key entry with the given `number` from the user's inventory. The next-number counter is NOT decremented — numbers are never reused after removal.","operationId":"post_RemovePhysicalKeyRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"integer","exclusiveMinimum":0,"description":"Number of the physical key to remove"},"_callerOrgId":{"type":"string","description":"Server-set: caller org id from API-key auth. Clients MUST NOT supply this."}},"required":["number"]}}}},"responses":{"200":{"description":"Physical key removed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"aggregateId":{"type":"string"},"version":{"type":"number"},"event":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["PhysicalKeyRemoved"]},"data":{"type":"object","properties":{"userId":{"type":"string"},"number":{"type":"number"},"removedAt":{"type":"number"}},"required":["userId","number","removedAt"]}},"required":["type","data"]},"number":{"type":"integer","exclusiveMinimum":0}},"required":["success","aggregateId","version","event","number"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"User not found, or no physical key with that number","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/users/{id}/PhysicalKeys":{"get":{"tags":["User","Query","Physical Keys"],"summary":"List / search the user's physical keys","description":"Lists physical-key entries for the user. Optional filters: `orgId` (exact), `orgName` (exact), `q` (case-insensitive substring match on `note`). Filters combine with AND. Capped at 100 entries; `total` reflects filtered count before the cap.","operationId":"get_SearchPhysicalKeysRoute","parameters":[{"schema":{"type":"string","description":"User ID"},"required":true,"description":"User ID","name":"id","in":"path"},{"schema":{"type":"string","description":"Filter by exact orgId match"},"required":false,"description":"Filter by exact orgId match","name":"orgId","in":"query"},{"schema":{"type":"string","description":"Filter by exact orgName match"},"required":false,"description":"Filter by exact orgName match","name":"orgName","in":"query"},{"schema":{"type":"string","description":"Case-insensitive substring match against the note field"},"required":false,"description":"Case-insensitive substring match against the note field","name":"q","in":"query"}],"responses":{"200":{"description":"Physical keys retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"object","properties":{"number":{"type":"integer","exclusiveMinimum":0},"orgId":{"type":"string"},"orgName":{"type":"string"},"note":{"type":"string"},"addedAt":{"type":"number"}},"required":["number","orgId","addedAt"]}},"total":{"type":"integer","minimum":0,"description":"Filtered count BEFORE the 100-result cap"}},"required":["keys","total"]}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["success","errors"]}}}}}}},"/admin/backfill-threedeye":{"post":{"tags":["Admin","3dEye"],"summary":"Backfill 3dEye accounts for existing users","description":"Finds users without 3dEye accounts and creates them. Requires admin auth.","operationId":"post_BackfillThreeDeyeRoute","parameters":[{"schema":{"type":"string","description":"Maximum number of users to process (default: 50)"},"required":false,"description":"Maximum number of users to process (default: 50)","name":"limit","in":"query"}],"responses":{"200":{"description":"Backfill completed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"total":{"type":"number"},"created":{"type":"number"},"failed":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["success","total","created","failed","errors"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/admin/test-error":{"get":{"tags":["Admin","Debug"],"summary":"Test error handling","description":"Admin-only debug endpoint that intentionally throws different types of errors to verify Sentry integration and error reporting pipelines. Supports synchronous errors, asynchronous errors, TypeError (property access on undefined), and validation errors. This endpoint should never return a 200 response in normal usage. Requires dual authentication: X-Admin-API-Key header and a JWT with @smartphonekey.com email domain.","operationId":"get_ErrorTestRoute","parameters":[{"schema":{"type":"string","enum":["sync","async","type","validation"],"default":"sync","description":"Type of error to throw"},"required":false,"description":"Type of error to throw","name":"type","in":"query"}],"responses":{"200":{"description":"Should never reach this response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"500":{"description":"Uncaught error thrown intentionally","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/admin/resync-projections":{"post":{"tags":["Admin","Projections"],"summary":"Trigger projection resync for all aggregates via Workflow","description":"Lists all aggregate IDs from R2 and starts a Cloudflare Workflow that processes DOs in small batches with configurable delays to avoid overwhelming SpacetimeDB. Returns the workflow instance ID for status tracking. Optionally filter by aggregate type. Requires dual auth: X-Admin-API-Key + JWT.","operationId":"post_ResyncProjectionsRoute","parameters":[{"schema":{"type":"string","enum":["user","lock","hub","matter-device","camera","camera-role","rtsp-camera"],"description":"Limit resync to a single aggregate type. Omit to resync all supported types."},"required":false,"description":"Limit resync to a single aggregate type. Omit to resync all supported types.","name":"aggregateType","in":"query"},{"schema":{"type":"string","description":"Optional single aggregate ID to resync. Requires aggregateType. When set, the R2 list step is skipped — the workflow enqueues just this one DO."},"required":false,"description":"Optional single aggregate ID to resync. Requires aggregateType. When set, the R2 list step is skipped — the workflow enqueues just this one DO.","name":"aggregateId","in":"query"},{"schema":{"type":"string","description":"Number of DOs to poke per batch (default: 20)"},"required":false,"description":"Number of DOs to poke per batch (default: 20)","name":"batchSize","in":"query"},{"schema":{"type":"string","description":"Seconds to wait between batches (default: 3)"},"required":false,"description":"Seconds to wait between batches (default: 3)","name":"delaySeconds","in":"query"}],"responses":{"200":{"description":"Workflow started — use the instanceId to check status","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"instanceId":{"type":"string"},"aggregateTypes":{"type":"array","items":{"type":"string"}},"total":{"type":"number"},"batchSize":{"type":"number"},"delaySeconds":{"type":"number"},"batches":{"type":"number"},"counts":{"type":"object","properties":{"user":{"type":"number"},"lock":{"type":"number"},"hub":{"type":"number"},"matter-device":{"type":"number"},"camera":{"type":"number"},"camera-role":{"type":"number"},"rtsp-camera":{"type":"number"}}},"errors":{"type":"array","items":{"type":"string"}}},"required":["success","instanceId","aggregateTypes","total","batchSize","delaySeconds","batches","counts","errors"]}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"403":{"description":"Forbidden — email domain not allowed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"},"message":{"type":"string"}},"required":["success","error","message"]}}}}}}},"/cameras/my":{"get":{"tags":["Camera","Query"],"summary":"List cameras accessible to the authenticated user","description":"Returns all cameras the authenticated B2C user can access via their camera role memberships. Permissions from multiple roles for the same camera are merged into a single entry.","operationId":"get_GetMyCamerasRoute","responses":{"200":{"description":"Camera list retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"cameras":{"type":"array","items":{"type":"object","properties":{"cameraId":{"type":"string","description":"Camera ID"},"threeDeyeCameraId":{"type":"string","description":"3dEye camera ID"},"name":{"type":"string","description":"Camera display name"},"type":{"type":"string","description":"Camera type (generic, onvif, pnp)"},"status":{"type":"string","description":"Camera status"},"httpUrl":{"type":"string","description":"HTTP access URL for the camera"},"rtspUrl":{"type":"string","description":"RTSP streaming URL for the camera"},"orgId":{"type":"string","description":"Organization ID"},"permissions":{"type":"array","items":{"type":"string"},"description":"Permissions granted to the user for this camera"}},"required":["cameraId","threeDeyeCameraId","name","type","status","httpUrl","rtspUrl","orgId","permissions"]},"description":"Cameras accessible to the authenticated user"},"total":{"type":"number","description":"Total number of cameras returned"}},"required":["cameras","total"]}}}},"401":{"description":"Unauthorized — JWT authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/temp-keys/{uuid}":{"get":{"tags":["TempKey","Web"],"summary":"Render temp key web page","description":"Returns an HTML page for a temporary key link. Validates the key status (expiration, usage limits, user authorization) and renders the appropriate UI. Public keys are accessible without authentication; user-whitelist keys require a valid JWT.","operationId":"get_GetTempKeyRoute","parameters":[{"schema":{"type":"string","description":"Temporary key UUID"},"required":true,"description":"Temporary key UUID","name":"uuid","in":"path"}],"responses":{"200":{"description":"Temporary key page rendered successfully","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"Temporary key not found","content":{"text/html":{"schema":{"type":"string"}}}},"500":{"description":"Internal server error","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/users/by-email":{"get":{"tags":["User","Query"],"summary":"Find user by email address","description":"Looks up a user by email address. If the user does not exist, creates them in both D1 (read model) and UserAggregate (event sourcing). This endpoint is idempotent — safe to call multiple times for the same email.","operationId":"get_GetUserByEmailRoute","parameters":[{"schema":{"type":"string","format":"email","description":"User email address"},"required":true,"description":"User email address","name":"email","in":"query"},{"schema":{"type":"string","description":"Optional UUID to assign to the new user"},"required":false,"description":"Optional UUID to assign to the new user","name":"uuid","in":"query"}],"responses":{"200":{"description":"User found or created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"uuid":{"type":"string"},"email":{"type":"string","format":"email"},"created_at":{"type":"string"}},"required":["uuid","email","created_at"]},"example":{"uuid":"550e8400-e29b-41d4-a716-446655440000","email":"user@example.com","created_at":"2024-01-15T10:30:00.000Z"}}}},"400":{"description":"Missing email parameter","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Database not available","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/users":{"post":{"tags":["User","Webhook"],"summary":"Webhook: create user from external event","description":"Handles Auth0 webhook events for user sign-up or first-time login. Ensures the user exists in both D1 (read model) and UserAggregate (event sourcing). Called automatically by Auth0 post-login/post-registration flows.","operationId":"post_CreateUserWebhookRoute","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"detail":{"type":"object","properties":{"data":{"type":"object","properties":{"user_name":{"type":"string","format":"email","description":"User email from Auth0"},"user_id":{"type":"string","description":"Auth0 user ID"}},"required":["user_name","user_id"]}},"required":["data"]}},"required":["detail"]}}}},"responses":{"200":{"description":"User created or found successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"userId":{"type":"string"},"email":{"type":"string","format":"email"}},"required":["success","userId","email"]}}}},"400":{"description":"Invalid Auth0 event format","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}}},"webhooks":{}}