{"openapi":"3.0.1","info":{"title":"Solvotix Building Automation API","description":"The Solvotix API provides access to building automation data, sensors, gateways,\nand tenant management features.\n\n**Authentication**\n---\nThis API supports two bearer-token authentication methods:\n\n1. **Solvotix API tokens** for integrations and machine-to-machine access. API tokens\n   start with `sat_`, are bound to one tenant, and do not require Firebase.\n2. **Firebase ID tokens** for interactive users signed in with email/password,\n   Google Sign-In, or another configured Firebase provider.\n\nInclude either token in the HTTP header:\n\n```\nAuthorization: Bearer sat_<API_TOKEN>\n```\n\nor\n\n```\nAuthorization: Bearer <FIREBASE_ID_TOKEN>\n```\n\nMost tenant-scoped requests should also include `Tenant: <TENANT_ID>`. A Solvotix API\ntoken can only access the tenant to which it is bound. API tokens can be created, rotated,\nand revoked through `/api/api-users` by an authorized interactive user.\n","contact":{"name":"Solvotix API Support","url":"https://solvotix.org","email":"support@solvotix.org"},"version":"1.0"},"servers":[{"url":"https://backend.solvotix.org","description":"Production Server"},{"url":"http://localhost:8080","description":"Local Development"}],"tags":[{"name":"Thermostat","description":"Endpoints for wall thermostat operations."},{"name":"Gateways","description":"Endpoints for managing **Solvotix Gateways** — including discovery, firmware updates,\nqueue synchronization, and remote node management.\nThese APIs are typically consumed by the Solvotix backend and mobile clients\nfor remote management and monitoring of deployed IoT gateways.\n"},{"name":"Wallet Certificates","description":"Manage tenant wallet cards and generate phone-installable Apple or Android Wallet packages."},{"name":"Sensors","description":"Endpoints for **managing Solvotix sensors** — including retrieval, configuration,\nBluetooth advertisement control, and command execution via connected gateways.\n"},{"name":"Automation","description":"Preconfigured building automation scenarios."},{"name":"API users","description":"Tenant-bound machine identities and API tokens"},{"name":"Automated Messages","description":"Predefined messages produced by automation triggers."},{"name":"Email Branding","description":"Tenant email layouts, uploaded logos and footer contact details. Configure in Settings → Messaging → Email branding (/home/settings#messaging). Tenant context is supplied globally by authentication."},{"name":"Bookings","description":"Manage bookings/rooms/categories via the main portal"},{"name":"AI Chat Threads","description":"Manage AI chat threads connected to bookings."},{"name":"Temperature Control","description":"Aggregate temperature statistics and control helpers."},{"name":"Browser session","description":"Firebase-backed browser session-cookie lifecycle; not used by API-user integrations"},{"name":"Node Updates","description":"List node firmware images and start node OTA updates."},{"name":"Lock Users","description":"Manage smart lock users and their access codes."},{"name":"Users","description":"Endpoints for **managing user accounts** within a Solvotix tenant.\nThese APIs allow tenant administrators to view members, invite new users,\nupdate profile information, and remove accounts securely.\n"},{"name":"Cleaning","description":"Track room cleaning status for cleaning staff."},{"name":"Tenants","description":"Endpoints for **managing organizational tenants** in the Solvotix IoT ecosystem.\nEach tenant represents an isolated environment (e.g., a building, facility, or organization)\nwith its own sensors, gateways, and users.\n"},{"name":"Smart Locks","description":"Endpoints for **managing smart locks** — including open/close commands,\ncode management, and configuration updates.\n"},{"name":"Lock Automation","description":"Scheduled opening and closing of locks, lock controllers and relays, reached in the portal\nunder **Automation → Lock Automation**.\n\nA task names a set of devices, one trigger, and a recurring schedule. Every time the\nschedule produces an occurrence, the trigger is queued on each device in the task.\n\n**Trigger behaviour per device family**\n\n| Trigger | Locks and lock controllers (`lock_*`) | Relays (`relay`, `high_voltage_relay`) |\n| --- | --- | --- |\n| `open` | Unlocks and stays open | Closes the circuit and stays closed until a `close` runs |\n| `close` | Locks | Opens the circuit |\n| `pulse_open` | Short open pulse, closes by itself | Pulse for the relay's configured `relay_open_ms` (default 5000 ms) |\n\n**Schedules** are evaluated in local time for the schedule's `timeZone`, defaulting to the\ntenant time zone, so an 08:00 task stays at 08:00 across daylight saving changes. Supported\n`frequency` values are `ONCE`, `DAILY`, `WEEKLY` and `MONTHLY`, combined with `interval`\n(every N units), `byWeekdays` for `WEEKLY`, and `byMonthDays` for `MONTHLY`. An 08:00 open\nand a 16:00 close are two separate tasks.\n\n**Execution guarantees.** The scheduler polls once a minute, so a trigger fires within about\na minute of its scheduled time. An occurrence is executed at most once. If the backend was\nnot running when an occurrence was due, it is skipped rather than fired late, once it falls\noutside the catch-up window (`lock.automation.catch-up-window-minutes`, default 10 minutes).\nTasks only run while the tenant has the **Lock Automation** module enabled under\n**Settings → Modules**; disabling the module stops all execution without deleting tasks.\n\nEvery executed and failed operation is written to the event log, so delivery can be verified\nthere per device.\n\nThese endpoints change physical access. They are restricted to superusers.\n"},{"name":"Guest Portal","description":"Endpoints for guest-facing booking and sensor controls."},{"name":"Protocol messages","description":"Recent in-memory gateway protocol traffic for the authenticated tenant"},{"name":"Guest Portal Admin","description":"Tenant admin endpoints for guest portal configuration."},{"name":"Modules","description":"Manage module activation per tenant."},{"name":"Relay","description":"Endpoints for relay including open/close and pulse commands."},{"name":"Events","description":"Endpoints for managing Solvotix events, including creation, retrieval, updates, and deletion."}],"paths":{"/api/wallet-certificates/{id}":{"get":{"tags":["Wallet Certificates"],"summary":"Get wallet certificate","operationId":"get","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Wallet certificate returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificate"}}}},"404":{"description":"Wallet certificate not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificate"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Wallet Certificates"],"summary":"Update wallet certificate","description":"Replaces editable card metadata. Blank companyName and logoUrl values restore the documented defaults.","operationId":"update","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificate"}}},"required":true},"responses":{"200":{"description":"Wallet certificate updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificate"}}}},"400":{"description":"Invalid payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificate"}}}},"404":{"description":"Wallet certificate not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificate"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Wallet Certificates"],"summary":"Delete wallet certificate","description":"Removes room and lock-user assignments, queues credential removal from affected locks, and deletes the tenant-owned card definition. Already-installed phone cards are not remotely removed.","operationId":"delete","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Wallet certificate deleted"},"404":{"description":"Wallet certificate not found"}},"security":[{"bearerAuth":[]}]}},"/api/users/{id}":{"get":{"tags":["Users"],"summary":"Get user","description":"Retrieves detailed information about a specific user by their unique ID.","operationId":"getUserById","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"u_001"}],"responses":{"200":{"description":"User retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"},"example":{"id":"u_001","email":"jane@solvotix.org","name":"Jane Doe"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Users"],"summary":"Update user","description":"Updates details for an existing user within the tenant.\nThis can include changing the display name, email, or role (depending on access permissions).\n","operationId":"updateUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"u_001"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"required":true},"responses":{"200":{"description":"User updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"description":"Invalid update request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Users"],"summary":"Remove user from tenant","description":"Removes a user from the current tenant.\nIf the user no longer belongs to any tenants, their Firebase account may be deleted\nautomatically by the backend for compliance.\n","operationId":"deleteUser","parameters":[{"name":"id","in":"path","description":"User ID to remove","required":true,"schema":{"type":"string"},"example":"u_001"}],"responses":{"200":{"description":"User removed successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"404":{"description":"User not found or already removed","content":{"application/json":{"schema":{"type":"boolean"}}}}},"security":[{"bearerAuth":[]}]}},"/api/users/pushnotification":{"put":{"tags":["Users"],"summary":"Register a user for push notifications","operationId":"updatePushNotifications","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevicePushRegistration"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Users"],"summary":"Register a user for push notifications","operationId":"registerForPushNotifications","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevicePushRegistration"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"bearerAuth":[]}]}},"/api/users/pushnotification/settings/{userId}":{"get":{"tags":["Users"],"summary":"Get push notification settings for a user","description":"Returns push notification category toggles for the specified user in the current tenant.","operationId":"getPushNotificationSettings","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPushNotificationSettings"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Users"],"summary":"Update push notification settings for a user","description":"Updates push notification category toggles for the specified user in the current tenant.","operationId":"updatePushNotificationSettings","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPushNotificationSettings"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPushNotificationSettings"}}}}},"security":[{"bearerAuth":[]}]}},"/api/users/me":{"get":{"tags":["Users"],"summary":"Get current user","description":"Retrieves the currently authenticated user from the request context.","operationId":"getCurrentUser","responses":{"200":{"description":"Current user retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Users"],"summary":"Save current user","description":"Saves current-user preferences, including the selected UI language.","operationId":"saveCurrentUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"required":true},"responses":{"200":{"description":"Current user saved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"description":"Invalid update request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}},"security":[{"bearerAuth":[]}]}},"/api/users/emailnotification":{"get":{"tags":["Users"],"summary":"List email notification toggles for a tenant","description":"Returns all user email notification toggles for the current tenant (derived from headers).","operationId":"getAllEmailNotifications","responses":{"200":{"description":"List returned successfully","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EmailNotificationRegistration"}},"example":[{"id":"eml_001","userId":"u_001","enabled":true},{"id":"eml_002","userId":"u_002","enabled":false}]}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Users"],"summary":"Update email notification toggle","description":"Updates the email notification preference (on/off). Include the id to overwrite an existing record.","operationId":"updateEmailNotifications","requestBody":{"description":"Existing registration with updated enabled flag","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailNotificationRegistration"},"example":{"id":"eml_001","enabled":false}}},"required":true},"responses":{"200":{"description":"Updated successfully"},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"404":{"description":"Registration not found"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Users"],"summary":"Enable/disable email notifications for current user","description":"Toggles email alerts for the authenticated user. Tenant is derived from headers.","operationId":"registerForEmailNotifications","requestBody":{"description":"Preference payload containing tenantId and enabled flag","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailNotificationRegistration"},"example":{"enabled":true}}},"required":true},"responses":{"200":{"description":"Registered/updated successfully"},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"}},"security":[{"bearerAuth":[]}]}},"/api/tenants/gateway-wifi":{"get":{"tags":["Tenants"],"summary":"List saved gateway Wi-Fi networks","description":"Returns all saved networks for reuse during gateway setup, including their unmasked passwords.\nThe response is an array of {ssid, password} entries, sorted by SSID using case-sensitive string order.\nEach exact SSID appears once. Previously saved single-network credentials remain available in this list.\nAuthenticate normally and provide the current Tenant header; API-user tokens derive their bound\ntenant when the header is omitted. There is no tenant argument in the path or body.\nRequires unrestricted access (an empty role list); Restricted and other role-limited users are denied.\nHuman users must belong to the selected tenant. Call only for an authorized gateway setup workflow;\nnever log, expose in agent output, or cache the response. Successful responses use Cache-Control: no-store.\nReturns 200 with [] when no networks are saved. This read is idempotent and sends no device commands or events.\nSaved credentials do not prove that a gateway is connected or that the network still works.\n","operationId":"getTenantGatewayWifiCredentials","responses":{"200":{"description":"Saved networks, or an empty array","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GatewayWifiCredentials"}}}}},"400":{"description":"Missing tenant context"},"401":{"description":"Missing or invalid authentication, or API-token tenant mismatch"},"403":{"description":"Role denied or tenant not accessible"},"404":{"description":"Tenant not found"}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Tenants"],"summary":"Save gateway Wi-Fi credentials for reuse","description":"After the app confirms a successful gateway Wi-Fi connection and the user authorizes saving it,\nadd the network to the tenant's saved list for later gateway setup. Saving an existing exact SSID\nupdates only that network's password; all other SSIDs remain saved. This accepts one entry per call,\nnot a replacement list. SSIDs differing in case or whitespace are distinct entries.\nExample body: {\"ssid\":\"Building Wi-Fi\",\"password\":\"<WIFI_PASSWORD>\"}.\nBoth keys are required: ssid is 1-32 UTF-8 bytes; password is 0-64 UTF-8 bytes.\nAn empty password represents an open network. Values are preserved exactly without trimming.\nUses authentication-filter tenant context, with the same authentication and access rules as GET.\nThis is security-sensitive credential storage: agents need explicit authorization to save or replace it.\nA 204 confirms persistence only; the backend does not verify connectivity, provision a gateway,\nqueue a command, or emit an event. Verify persistence with GET /api/tenants/gateway-wifi.\nRepeating the same PUT is idempotent; the last storage write for the same SSID wins. After an ambiguous failure,\nread first and retry only if the intended network is still current, to avoid overwriting a newer save.\nOrdinary tenant updates do not change these credentials. Never log request bodies or passwords.\n","operationId":"saveTenantGatewayWifiCredentials","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayWifiCredentials"}}},"required":true},"responses":{"204":{"description":"Credentials saved; no response body"},"400":{"description":"Invalid payload or missing tenant context"},"401":{"description":"Missing or invalid authentication, or API-token tenant mismatch"},"403":{"description":"Role denied or tenant not accessible"},"404":{"description":"Tenant not found"}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Tenants"],"summary":"Forget one saved gateway Wi-Fi network","description":"Deletes only the saved network matching the required ssid query parameter, including its password.\nOther saved networks remain available. Example: DELETE /api/tenants/gateway-wifi?ssid=Building%20Wi-Fi.\nURL-encode the exact SSID with the client's query-parameter encoder, including +, &, # and whitespace.\nUses the same authentication and access rules as GET; no request body or tenant argument.\nOmitting ssid is a 400 error, never a request to delete all networks.\nAgents need explicit authorization to forget the saved secret. Returns 204 even when this SSID is absent.\nDoes not disconnect gateways, erase credentials on devices, send commands, or emit events.\nVerify with GET /api/tenants/gateway-wifi and confirm this SSID is absent from the list.\nDeletion is idempotent; after an ambiguous\nfailure, read first and do not automatically delete a network saved by another caller in the meantime.\n","operationId":"deleteTenantGatewayWifiCredentials","parameters":[{"name":"ssid","in":"query","description":"Exact SSID to forget, 1-32 UTF-8 bytes; URL-encode as a query parameter. Case and whitespace are significant.","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Saved network absent; no response body"},"400":{"description":"Missing or invalid SSID, or missing tenant context"},"401":{"description":"Missing or invalid authentication, or API-token tenant mismatch"},"403":{"description":"Role denied or tenant not accessible"},"404":{"description":"Tenant not found"}},"security":[{"bearerAuth":[]}]}},"/api/tenants":{"get":{"tags":["Tenants"],"summary":"List all tenants associated with the current user","description":"Returns all tenant objects that the authenticated user has access to.\nIf the user has no existing tenants, a new one may be automatically created\nand assigned to them for convenience.\n","operationId":"getTenants","responses":{"200":{"description":"List of tenants returned successfully","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Tenant"}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Tenant"}}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Tenants"],"summary":"Update tenant information","description":"Updates a tenant’s name, contactEmail, diagnosticEmail, contactPhoneNumber, contactName, metadata, or configuration. The roomCodeLength setting accepts\n4 through 7 and defaults to 4 when omitted; it controls the length of newly generated\nand selected room codes without changing existing codes or queueing a device configuration message.\nWhen the effective sound level, system code, NFC-reader setting, or smart lock access mode changes,\nqueues the tenant's updated configuration for every device. Other tenant changes do not queue device messages.\nConfiguration messages do not update device time; devices update time when they boot\nand request a time anchor.\nThe authenticated user must be a member of the tenant to perform this action.\n","operationId":"saveTenant","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}},"required":true},"responses":{"200":{"description":"Tenant successfully updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"400":{"description":"Room code length must be between 4 and 7","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"404":{"description":"Tenant not found or not accessible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Tenants"],"summary":"Create a new tenant","description":"Creates a new tenant and automatically associates it with the currently authenticated user.\nTypically used during onboarding or when adding new organizational sites.\n","operationId":"createTenant","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}},"required":true},"responses":{"200":{"description":"Tenant successfully created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"400":{"description":"Invalid tenant payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Tenants"],"summary":"Delete a tenant","description":"Deletes a tenant from the Solvotix system, removing its association with the current user.\nIf no other users are associated with the tenant, it will be permanently deleted.\nLast-user cleanup also removes gateway registrations, releases global gateway claims,\ndisconnects registered transports, clears runtime gateway state and cached gateway\nqueues, and releases node connection ownership. These gateway IDs can subsequently\nbe claimed in another tenant with POST /api/gateways/{id}/{name}. Shared tenants\nretain their gateways. This does not factory-reset hardware or acknowledge cancellation\nof commands already delivered to it.\n\nRequires bearer authentication, the Tenant header and a Tenant request body identifying\nthe target, for example {\"id\":\"<TENANT_ID>\"}. Obtain explicit approval for this\ndestructive operation and confirm remaining memberships first. Returns 200 with the\nsupplied Tenant body, including when no accessible matching tenant was found; the\nechoed body alone does not verify deletion. Verify remaining tenant access and, when\nauthorized, gateway state. No dedicated gateway-deletion event is emitted. After an\nambiguous failure, inspect state before retrying because cleanup is not atomic.\n","operationId":"deleteTenant","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}},"required":true},"responses":{"200":{"description":"Supplied tenant returned after membership removal and any last-user cleanup","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}}},"security":[{"bearerAuth":[]}]}},"/api/tenants/":{"get":{"tags":["Tenants"],"summary":"List all tenants associated with the current user","description":"Returns all tenant objects that the authenticated user has access to.\nIf the user has no existing tenants, a new one may be automatically created\nand assigned to them for convenience.\n","operationId":"getTenants_1","responses":{"200":{"description":"List of tenants returned successfully","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Tenant"}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Tenant"}}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Tenants"],"summary":"Update tenant information","description":"Updates a tenant’s name, contactEmail, diagnosticEmail, contactPhoneNumber, contactName, metadata, or configuration. The roomCodeLength setting accepts\n4 through 7 and defaults to 4 when omitted; it controls the length of newly generated\nand selected room codes without changing existing codes or queueing a device configuration message.\nWhen the effective sound level, system code, NFC-reader setting, or smart lock access mode changes,\nqueues the tenant's updated configuration for every device. Other tenant changes do not queue device messages.\nConfiguration messages do not update device time; devices update time when they boot\nand request a time anchor.\nThe authenticated user must be a member of the tenant to perform this action.\n","operationId":"saveTenant_1","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}},"required":true},"responses":{"200":{"description":"Tenant successfully updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"400":{"description":"Room code length must be between 4 and 7","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"404":{"description":"Tenant not found or not accessible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Tenants"],"summary":"Create a new tenant","description":"Creates a new tenant and automatically associates it with the currently authenticated user.\nTypically used during onboarding or when adding new organizational sites.\n","operationId":"createTenant_1","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}},"required":true},"responses":{"200":{"description":"Tenant successfully created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"400":{"description":"Invalid tenant payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/configuration":{"put":{"tags":["Smart Locks"],"summary":"Set smart lock configuration","description":"Updates sound level, light setting, open duration, six-digit system-menu code, and boot-time clock synchronization.","operationId":"setConfiguration","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationRequest"}}},"required":true},"responses":{"200":{"description":"Configuration command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid sensor or payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/configuration/global":{"get":{"tags":["Smart Locks"],"summary":"Get global smart lock configuration","description":"Returns tenant-level configuration applied to all smart locks.","operationId":"getGlobalConfiguration","responses":{"200":{"description":"Configuration returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockGlobalConfiguration"}}}},"400":{"description":"Missing tenant context","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockGlobalConfiguration"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Smart Locks"],"summary":"Save global smart lock configuration","description":"Updates tenant-level configuration applied to all smart locks.","operationId":"saveGlobalConfiguration","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartLockGlobalConfiguration"}}},"required":true},"responses":{"200":{"description":"Configuration saved","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockGlobalConfiguration"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockGlobalConfiguration"}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/configuration/access-mode":{"get":{"tags":["Smart Locks"],"summary":"Get global smart lock access mode","description":"Returns the tenant-wide access mode applied to every keypad lock, lock controller and\nwireless code panel, together with the list of accepted values.\n\nModes:\n* `standard` — a valid code opens the lock and it closes again on its own.\n* `openUntilClosed` — a valid code keeps the lock open until `*` is pressed on the\n  keypad, a close command is sent, or the physical system menu locks it.\n* `forcedClosed` — local codes and cards are refused and reported as invalid, so the\n  lock can only be opened from the backend or from the physical system menu.\n\nIn the portal this is found under **Settings** → smart lock settings, next to the\nNFC reader toggle.\n","operationId":"getAccessMode","responses":{"200":{"description":"Access mode returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockAccessModeConfiguration"}}}},"400":{"description":"Missing tenant context","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockAccessModeConfiguration"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Smart Locks"],"summary":"Save global smart lock access mode","description":"Sets the tenant-wide access mode and queues the updated boot configuration for every\ndevice in the tenant. Accepted values are `standard`, `openUntilClosed` and\n`forcedClosed`; any other value is rejected with 400.\n\nDelivery is asynchronous: a device applies the mode when the queued message reaches it,\nso a node that is currently out of range keeps its previous mode until it is heard from\nagain. The mode a device actually runs is reported back with its next time request and\nstored on the sensor as `reportedAccessMode`.\n\n`forcedClosed` stops every local code and card from opening the lock, and also closes a\nlock that `openUntilClosed` was holding open. Opening then requires this API or the\nsix-digit physical system menu, so confirm with the operator before switching a tenant\ninto it.\n","operationId":"saveAccessMode","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartLockAccessModeConfiguration"}}},"required":true},"responses":{"200":{"description":"Access mode saved and queued for all devices","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockAccessModeConfiguration"}}}},"400":{"description":"Missing tenant context or unknown access mode","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockAccessModeConfiguration"}}}},"404":{"description":"Tenant not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockAccessModeConfiguration"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/{id}":{"get":{"tags":["Sensors"],"summary":"Get sensor by ID","description":"Fetches details of a specific sensor by its unique ID.","operationId":"getSensorById","parameters":[{"name":"id","in":"path","description":"Unique sensor ID","required":true,"schema":{"type":"string"},"example":"sensor_AA11BB22"}],"responses":{"200":{"description":"Sensor found and returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sensor"}}}},"404":{"description":"Sensor not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sensor"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Sensors"],"summary":"Update a sensor","description":"Updates metadata or configuration of a given sensor.\n\nThe body must contain a valid Sensor object. Returns the updated sensor entity on success.\n","operationId":"updateSensor","parameters":[{"name":"id","in":"path","description":"Sensor ID to update","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sensor"}}},"required":true},"responses":{"200":{"description":"Sensor updated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Sensor"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Sensor"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Sensors"],"summary":"Delete a sensor","description":"Removes the sensor security association, tenant sensor record and all backend code-slot records for this device, including pending uploads/removals and extended credentials. Requires bearer authentication and unrestricted or Restricted tenant access; Tenant context is applied by the authentication filter. Destructive operation: confirm the tenant and device ID and obtain explicit approval. No request body. Does not erase credentials from the physical lock, queue a removal command, cancel existing commands, or remove room/lock-user references. No deletion event is emitted. Returns 204 with no body, including when the device is already absent; repeating deletion also cleans leftover code-slot records. Verify absence through GET /api/sensor. The deletes are not transactional: after a failure, inspect inventory and confirm that the device has not been reclaimed before retrying. Missing devices are excluded from delayed-code warnings.","operationId":"deleteSensor","parameters":[{"name":"id","in":"path","description":"Sensor ID to delete","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Sensor and backend code-slot records deleted or already absent"},"401":{"description":"Authentication failed or API-token tenant mismatch"},"403":{"description":"Role does not permit device deletion"}},"security":[{"bearerAuth":[]}]}},"/api/sensor/{id}/pairings":{"get":{"tags":["Sensors"],"summary":"Get persisted paired devices for a sensor","description":"Returns the currently stored pairing list from the backend sensor document.","operationId":"getPairings","parameters":[{"name":"id","in":"path","description":"Sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Pairings returned","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SensorPairedDevice"}}}}},"404":{"description":"Sensor not found","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SensorPairedDevice"}}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Sensors"],"summary":"Replace paired devices on a sensor","description":"Queues a pairing write for the sensor and persists the requested paired device list locally as pending.","operationId":"updatePairings","parameters":[{"name":"id","in":"path","description":"Sensor ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorPairingRequest"}}},"required":true},"responses":{"200":{"description":"Pairing write queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid sensor or pair payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/temperature-control/configuration":{"get":{"tags":["Sensors"],"summary":"Get temperature control configuration","description":"Retrieves the per-tenant temperature control configuration, creating it if missing.","operationId":"getTemperatureControlConfiguration","responses":{"200":{"description":"Configuration returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemperatureControlConfiguration"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Sensors"],"summary":"Save temperature control configuration","description":"Creates or updates the per-tenant temperature control configuration.","operationId":"saveTemperatureControlConfiguration","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemperatureControlConfiguration"}}},"required":true},"responses":{"200":{"description":"Configuration saved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemperatureControlConfiguration"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/groups":{"put":{"tags":["Sensors"],"summary":"Create or update a sensor group","description":"Creates a new group when id is missing, otherwise updates the existing group.","operationId":"saveSensorGroup","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorGroup"}}},"required":true},"responses":{"200":{"description":"Group saved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorGroup"}}}}},"security":[{"bearerAuth":[]}]}},"/api/relay/{id}/default-milliseconds":{"put":{"tags":["Relay"],"summary":"Set relay default milliseconds","description":"Sets `relay.configuration.relay_open_ms` used by `POST /open` and `POST /pulse-open`.","operationId":"setDefaultMilliseconds","parameters":[{"name":"id","in":"path","description":"Relay ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RelayConfigurationRequest"}}},"required":true},"responses":{"200":{"description":"Default milliseconds saved","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Sensor"}}}},"400":{"description":"Invalid relay or payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Sensor"}}}},"404":{"description":"Relay not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Sensor"}}}}},"security":[{"bearerAuth":[]}]}},"/api/modules/settings":{"get":{"tags":["Modules"],"summary":"Get tenant module settings","description":"Returns automation and integration modules with activation state for the current tenant.","operationId":"getSettings","responses":{"200":{"description":"Module settings loaded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantModuleSettingsResponse"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Modules"],"summary":"Update tenant module settings","description":"Persists module activation per tenant. Unknown modules are ignored.","operationId":"updateSettings","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantModuleSettingsRequest"}}},"required":true},"responses":{"200":{"description":"Module settings saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantModuleSettingsResponse"}}}}},"security":[{"bearerAuth":[]}]}},"/api/lockusers/{id}":{"get":{"tags":["Lock Users"],"summary":"Get lock user","description":"Returns computed numeric-code upload status: codesNotUploadedToAllSensors counts pending lock/code pairs; sensorIdsWithCodesNotUploaded lists the distinct affected code-capable sensor IDs. Confirmation requires ADDED_TO_LOCK state and uploadedToLockAt. Credential masking does not affect the device IDs. Reading does not queue commands.","operationId":"getById","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Lock user returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"404":{"description":"Lock user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Lock Users"],"summary":"Update lock user","description":"Updates a lock user and syncs changed codes/sensors to assigned lock sensors.","operationId":"update_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}},"required":true},"responses":{"200":{"description":"Lock user updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"400":{"description":"Invalid payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"404":{"description":"Lock user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Lock Users"],"summary":"Delete lock user","description":"Deletes a lock user and queues removal of no-longer-referenced sensor/code pairs.","operationId":"delete_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Lock user deleted"},"404":{"description":"Lock user not found"}},"security":[{"bearerAuth":[]}]}},"/api/lock-users/{id}":{"get":{"tags":["Lock Users"],"summary":"Get lock user","description":"Returns computed numeric-code upload status: codesNotUploadedToAllSensors counts pending lock/code pairs; sensorIdsWithCodesNotUploaded lists the distinct affected code-capable sensor IDs. Confirmation requires ADDED_TO_LOCK state and uploadedToLockAt. Credential masking does not affect the device IDs. Reading does not queue commands.","operationId":"getById_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Lock user returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"404":{"description":"Lock user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Lock Users"],"summary":"Update lock user","description":"Updates a lock user and syncs changed codes/sensors to assigned lock sensors.","operationId":"update_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}},"required":true},"responses":{"200":{"description":"Lock user updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"400":{"description":"Invalid payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"404":{"description":"Lock user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Lock Users"],"summary":"Delete lock user","description":"Deletes a lock user and queues removal of no-longer-referenced sensor/code pairs.","operationId":"delete_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Lock user deleted"},"404":{"description":"Lock user not found"}},"security":[{"bearerAuth":[]}]}},"/api/guest-portal-admin/settings":{"get":{"tags":["Guest Portal Admin"],"summary":"Get guest portal settings for current tenant","operationId":"getGuestPortalSettings","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalSettings"}}}}}},"put":{"tags":["Guest Portal Admin"],"summary":"Update guest portal settings for current tenant","operationId":"updateGuestPortalSettings","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalSettings"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalSettings"}}}}}}},"/api/guest-portal-admin/languages/activated":{"get":{"tags":["Guest Portal Admin"],"summary":"Get activated guest portal languages for current tenant","operationId":"getActivatedLanguages","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GuestPortalLanguageOption"}}}}}}},"put":{"tags":["Guest Portal Admin"],"summary":"Update activated guest portal languages for current tenant","operationId":"updateActivatedLanguages","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalActivatedLanguagesRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GuestPortalLanguageOption"}}}}}}}},"/api/guest-portal-admin/direction-routes/{routeId}":{"get":{"tags":["Guest Portal Admin"],"summary":"Get a guest direction route","operationId":"getDirectionRoute","parameters":[{"name":"routeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalDirectionRoute"}}}}}},"put":{"tags":["Guest Portal Admin"],"summary":"Update a guest direction route","operationId":"updateDirectionRoute","parameters":[{"name":"routeId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalDirectionRoute"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalDirectionRoute"}}}}}},"delete":{"tags":["Guest Portal Admin"],"summary":"Delete a guest direction route","operationId":"deleteDirectionRoute","parameters":[{"name":"routeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/api/gateways/{id}":{"get":{"tags":["Gateways"],"summary":"Get a gateway by ID","description":"Retrieve detailed information about a specific gateway by its unique identifier.","operationId":"getById_2","parameters":[{"name":"id","in":"path","description":"Unique identifier of the gateway","required":true,"schema":{"type":"string"},"example":"gw_64F2D9B"}],"responses":{"200":{"description":"Gateway found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Gateway"}}}},"404":{"description":"Gateway not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Gateway"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Gateways"],"summary":"Update an existing gateway","description":"Updates the configuration or metadata of the gateway identified by the URL id. Include existing editable fields in the body; any body id is ignored. Set pushNotificationDelayMinutes to a positive number of minutes to control when an absent heartbeat marks the gateway offline and sends an offline warning; null or nonpositive values use the five minute default.","operationId":"update_3","parameters":[{"name":"id","in":"path","description":"Gateway ID to update","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gateway"}}},"required":true},"responses":{"200":{"description":"Gateway updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gateway"}}}},"404":{"description":"Gateway not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gateway"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Gateways"],"summary":"Delete a gateway","description":"Removes the gateway registration from the authenticated tenant and releases its global\nownership claim, allowing the physical gateway ID to be claimed by another tenant using\nPOST /api/gateways/{id}/{name}. Requires bearer authentication and the Tenant header;\nno request body is used. Confirm the physical gateway ID, current tenant and approval\nfor this ownership-changing operation before calling.\n\nClears the active gateway connection, runtime gateway state, cached gateway queue and\nnode connection ownership. Incoming traffic cannot register an unclaimed gateway.\nThis is a backend removal, not a hardware factory reset or deletion of associated\ndevices, their ownership, historical events or metering records. It does not confirm\ncancellation of commands already delivered to hardware.\n\nSuccess returns 204 with no body. An absent gateway or a gateway owned by another\ntenant returns 404 without releasing that tenant's claim. Verify removal with\nGET /api/gateways/{id} (404) and GET /api/gateways. There is no dedicated deletion\nevent or queue acknowledgement to wait for. After an ambiguous response, read state\nbefore retrying; a repeated delete of an absent gateway returns 404. Claim in the new\ntenant only after removal is verified, then verify its gateway list and details.\n","operationId":"deleteGateway","parameters":[{"name":"id","in":"path","description":"Unique gateway ID to delete","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Gateway deleted successfully"},"404":{"description":"Gateway not found"}},"security":[{"bearerAuth":[]}]}},"/api/events/{id}":{"get":{"tags":["Events"],"summary":"Get event by ID","description":"Retrieves a single event by its unique identifier.\nReturns 404 if the event does not exist.\n","operationId":"getEventById","parameters":[{"name":"id","in":"path","description":"The unique ID of the event","required":true,"schema":{"type":"string"},"example":"652af18e4b1d882f9c923d7a"}],"responses":{"200":{"description":"Event found and returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"}}}},"404":{"description":"Event not found"}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Events"],"summary":"Update existing event","description":"Updates an existing event by ID.\nThe provided `Event` body replaces existing data for that event.\n","operationId":"updateEvent","parameters":[{"name":"id","in":"path","description":"The unique ID of the event to update","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"}}},"required":true},"responses":{"200":{"description":"Event updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"}}}},"404":{"description":"Event not found"}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Events"],"summary":"Delete an event","description":"Deletes an event permanently from the Solvotix database.\nUse with caution — this operation cannot be undone.\n","operationId":"deleteEvent","parameters":[{"name":"id","in":"path","description":"The unique ID of the event to delete","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Event deleted successfully"},"404":{"description":"Event not found"}},"security":[{"bearerAuth":[]}]}},"/api/email-branding":{"get":{"tags":["Email Branding"],"summary":"Get email branding settings","description":"Returns the current tenant branding, including template, Modern-template primaryColor/secondaryColor and uploaded logoBase64 (creates an empty record if missing). Missing template renders as classic. Missing Modern colors use the built-in palette. Use bearer authentication and the selected Tenant header; API users are tenant-bound. Requires unrestricted access or Restricted role under the shared role policy. No email, device queue entry or event is created. GET is safe to retry. A successful read verifies saved configuration only, not email delivery.","operationId":"getSettings_1","responses":{"200":{"description":"Settings returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailBrandingSettings"}}}},"400":{"description":"Missing tenant context or invalid template, color, logo, URL or JSON input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailBrandingSettings"}}}},"401":{"description":"Missing/invalid authentication or API-user tenant mismatch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailBrandingSettings"}}}},"403":{"description":"Tenant access or role denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailBrandingSettings"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Email Branding"],"summary":"Update email branding settings","description":"In Settings → Messaging → Email branding, choose Classic, Modern, Minimal or Elegant, upload a PNG/JPEG logo and select Save email branding. PUT persists branding for future emails sent through the branded mail service. Modern accepts optional primaryColor and secondaryColor six-digit hex codes; omitted/null preserves them, blank resets the corresponding built-in color, and other templates ignore them. Existing text/URL fields are replaced (omitted/blank clears them); omit template/logoBase64 to preserve them for legacy clients. Empty template resets to classic; empty logoBase64 removes the upload and allows logoUrl fallback. Logo must be a base64 data URI, at most 512 KiB decoded and 2048 pixels per side; uploaded logos are embedded as CID MIME parts, not remote URLs. URLs must be absolute HTTP(S) without credentials. Name, addressLine, supportEmail and supportPhone are escaped display text; support fields do not change SMTP or Reply-To. Bearer authentication and global Tenant context are required, with unrestricted access or Restricted role. Risk is reversible tenant-wide presentation change: require authorization to change branding, but no physical-operation approval. Repeating the same body is idempotent; after an ambiguous save, GET and compare before retrying to avoid overwriting a newer edit. Returns saved settings, including logoBase64 and custom colors. No email is sent and no queue entry or event is created. Browser preview is representative; verify actual rendering with a separately authorized email send.","operationId":"saveSettings","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailBrandingSettings"}}},"required":true},"responses":{"200":{"description":"Settings saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailBrandingSettings"}}}},"400":{"description":"Missing tenant context or invalid template, color, logo, URL or JSON input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailBrandingSettings"}}}},"401":{"description":"Missing/invalid authentication or API-user tenant mismatch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailBrandingSettings"}}}},"403":{"description":"Tenant access or role denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailBrandingSettings"}}}}},"security":[{"bearerAuth":[]}]}},"/api/cleaning/settings":{"get":{"tags":["Cleaning"],"summary":"Get cleaning program settings","operationId":"getCleaningProgramSettings","responses":{"200":{"description":"Cleaning settings returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleaningProgramSettings"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Cleaning"],"summary":"Update cleaning program settings","operationId":"updateCleaningProgramSettings","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleaningProgramSettings"}}},"required":true},"responses":{"200":{"description":"Cleaning settings updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleaningProgramSettings"}}}}},"security":[{"bearerAuth":[]}]}},"/api/cleaning/rooms/{roomId}":{"get":{"tags":["Cleaning"],"summary":"Get room cleaning status","operationId":"getRoom_1","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cleaning status returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleaningRoom"}}}},"404":{"description":"Room not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleaningRoom"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Cleaning"],"summary":"Update room cleaning status","description":"Allows cleaning staff to mark a room as cleaned or reset it back to not cleaned.","operationId":"updateCleaningStatus","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleaningStatusUpdateRequest"}}},"required":true},"responses":{"200":{"description":"Cleaning status updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleaningRoom"}}}},"404":{"description":"Room not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleaningRoom"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms/{roomId}":{"put":{"tags":["Bookings"],"summary":"Update room","operationId":"updateRoom","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}},"required":true},"responses":{"200":{"description":"Updated room","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}}},"404":{"description":"Room not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Bookings"],"summary":"Delete room","operationId":"deleteRoom","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms/code-generation-settings":{"put":{"tags":["Bookings"],"summary":"Update automatic room-code digit exclusions","description":"Applies the same excluded decimal digits to the selected rooms. Unused conflicting codes are removed from storage and queued for removal from assigned locks, then replaced with compliant codes. Active booking codes are retained until their booking releases them. Lock delivery is asynchronous.","operationId":"updateRoomCodeGenerationSettings","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeGenerationSettingsRequest"}}},"required":true},"responses":{"200":{"description":"Settings saved and lock commands queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeGenerationSettingsResponse"}}}},"400":{"description":"Invalid room selection or digit policy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeGenerationSettingsResponse"}}}},"404":{"description":"A selected room was not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeGenerationSettingsResponse"}}}},"409":{"description":"A compliant replacement code could not be generated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeGenerationSettingsResponse"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/categories/{categoryId}":{"put":{"tags":["Bookings"],"summary":"Update category","operationId":"updateCategory","parameters":[{"name":"categoryId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Categories"}}},"required":true},"responses":{"200":{"description":"Updated category","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Categories"}}}},"404":{"description":"Category not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Categories"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Bookings"],"summary":"Delete category","operationId":"deleteCategory","parameters":[{"name":"categoryId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"}},"security":[{"bearerAuth":[]}]}},"/api/bookings/bookings/{bookingId}":{"put":{"tags":["Bookings"],"summary":"Update booking","description":"Updates a booking by bookingId.","operationId":"updateBooking","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}},"required":true},"responses":{"200":{"description":"Updated booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"description":"Booking not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Bookings"],"summary":"Delete booking","description":"Deletes a booking by bookingId.","operationId":"deleteBooking","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"}},"security":[{"bearerAuth":[]}]}},"/api/automation/messages/triggers/{id}":{"put":{"tags":["Automated Messages"],"summary":"Update trigger","description":"Updates an automation trigger with recipient, channel, content, room-cleanliness, or payment gating settings. Templates support {departureDate} for the booking end date in the tenant timezone and regional numeric format (Europe/Oslo: 18.09.2026; America/New_York: 09/18/2026), and {departureDateText} for a lowercase English date such as 18 sep 2026. Both work in the subject, general, email and SMS bodies, including inserted room instructions. Missing booking dates produce an empty value. Missing or invalid tenant timezones use UTC; timezones without a supported country convention use yyyy-MM-dd. The booking language selects room templates but does not change date formatting. {arrivalDate} and {arrivalDateText} format the booking start with the same numeric and text rules.  Use {guestportalbutton=\"Open guest portal\", color=\"#1068F0\", textColor=\"#F8F8F8\"} as a standalone body placeholder (not inside an HTML tag or link). Only email bodies render a clickable button; SMS and subjects leave this token unchanged. Use {guestportal} there. {guestportalbutton} defaults to label Open guest portal. Omitted or invalid colors default to Solvotix action blue #1068F0; color accepts #RGB or #RRGGBB. Text defaults to #F8F8F8, with #001838 on light custom backgrounds. Optional textColor overrides the text using #RGB or #RRGGBB; omitted or invalid textColor retains automatic text color. When both are supplied, color precedes textColor. Labels are HTML-escaped. Requires a booking ID and tenant context; otherwise the placeholder remains unchanged. Supported in email bodies, including general templates used as email fallback, inserted room instructions and manual email overrides. Existing {guestportal} remains a plain URL. sendOnlyWhenRoomClean and onlySendIfPaidOrCheckedIn are valid only with sendToBookingGuest. onlySendIfPaidOrCheckedIn holds delivery until the booking is paid in full (amountPaid at least totalAmount minus a tolerance of 2, and totalAmount above zero) or the guest is checked in. Omitting either flag stores it as false. offsetMinutes shifts when_start_time_has_passed relative to booking start: negative values send earlier, positive values later (-60 is one hour early; 60 is one hour late). Omitted or null stores 0; other trigger types ignore it. The threshold is booking start plus offsetMinutes; end and checkout are unchanged. Editing when_start_time_has_passed does not exclude bookings whose start precedes the edit or resend already delivered channels. Active, unsent bookings remain eligible until booking end or checkout once their offset threshold is reached, including bookings imported after their arrival time. Authenticate with a bearer token and tenant context supplied through the Tenant header. HTTP 200 returns the saved configuration. Changing the offset can make unsent bookings immediately eligible; confirm the audience and content first. After an uncertain update, read GET /api/automation/messages/triggers before retrying. Verify channels using GET /api/automation/messages/logs/bookings/{bookingId}; configuration success does not prove delivery. No new event or queue payload is introduced.","operationId":"updateTrigger","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationTriggerRequest"}}},"required":true},"responses":{"200":{"description":"Trigger updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationTrigger"}}}},"404":{"description":"Trigger not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationTrigger"}}}},"400":{"description":"Invalid trigger request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationTrigger"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Automated Messages"],"summary":"Delete trigger","description":"Deletes an automation trigger by id.","operationId":"deleteTrigger","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Trigger not found"}},"security":[{"bearerAuth":[]}]}},"/api/automation/locks/tasks/{id}":{"put":{"tags":["Lock Automation"],"summary":"Update a scheduled lock automation task","description":"Replaces the name, comment, trigger, devices, schedule and enabled state of an\nexisting task. All updatable fields are taken from the request body, so send the\ncomplete task, not a partial one. `userId` and `createdDate` are preserved.\n\nChanging the schedule does not replay occurrences that were missed while the old\nschedule was in force. The already-fired marker is kept, so an occurrence that\nalready ran under the previous schedule will not run again.\n\nSet `enabled` to false to stop a task from firing while keeping its configuration.\n","operationId":"updateTask","parameters":[{"name":"id","in":"path","description":"Task id","required":true,"schema":{"type":"string"},"example":"8f14e45f-ceea-467a-9575-4a1f3b7c2d90"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationTaskRequest"}}},"required":true},"responses":{"200":{"description":"Task updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationTask"}}}},"400":{"description":"Invalid task, schedule, trigger or device list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationTask"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationTask"}}}},"403":{"description":"Superuser access required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationTask"}}}},"404":{"description":"Task not found for the current tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationTask"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Lock Automation"],"summary":"Delete a scheduled lock automation task","description":"Permanently deletes the task, which stops all future occurrences. Commands already\nqueued for a gateway are not recalled.\n\nDeleting is safe to retry: a repeated delete of an already-deleted task returns 404.\n","operationId":"deleteTask","parameters":[{"name":"id","in":"path","description":"Task id","required":true,"schema":{"type":"string"},"example":"8f14e45f-ceea-467a-9575-4a1f3b7c2d90"}],"responses":{"204":{"description":"Task deleted"},"401":{"description":"Missing or invalid authentication"},"403":{"description":"Superuser access required"},"404":{"description":"Task not found for the current tenant"}},"security":[{"bearerAuth":[]}]}},"/api/api-users/{id}/roles":{"put":{"tags":["API users"],"summary":"Update API-user roles","description":"Replaces the roles for a tenant-bound API user. Changes apply to its next authenticated request and do not rotate the token. Empty roles grant unrestricted access. Interactive users only.","operationId":"updateRoles","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUserRolesRequest"}}},"required":true},"responses":{"200":{"description":"API-user roles updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUser"}}}},"400":{"description":"A supplied role is unknown","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUser"}}}},"403":{"description":"Caller is an API user or lacks tenant access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUser"}}}},"404":{"description":"API user not found in the selected tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/ai/settings":{"get":{"tags":["AI Chat Threads"],"summary":"Get AI settings","description":"Returns tenant-level AI settings including free-text instructions.","operationId":"getSettings_2","responses":{"200":{"description":"Settings returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AISettings"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["AI Chat Threads"],"summary":"Save AI settings","description":"Stores tenant-level AI settings including free-text instructions.","operationId":"updateSettings_1","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AISettings"}}},"required":true},"responses":{"200":{"description":"Settings saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AISettings"}}}},"400":{"description":"Invalid settings payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AISettings"}}}}},"security":[{"bearerAuth":[]}]}},"/api/wallet-certificates":{"get":{"tags":["Wallet Certificates"],"summary":"List wallet certificates","description":"Lists wallet card definitions for the authenticated tenant.","operationId":"list","responses":{"200":{"description":"Wallet certificates returned","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WalletCertificate"}}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Wallet Certificates"],"summary":"Create wallet certificate","description":"Creates a tenant-owned Apple or Android wallet card. The tenant comes from authentication context; signing secrets are server configuration and cannot be supplied here.","operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificate"}}},"required":true},"responses":{"200":{"description":"Wallet certificate created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificate"}}}},"400":{"description":"Invalid payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificate"}}}}},"security":[{"bearerAuth":[]}]}},"/api/wallet-certificates/{id}/package":{"post":{"tags":["Wallet Certificates"],"summary":"Generate wallet install package","description":"Generates a fresh phone-install artifact. Apple returns a signed application/vnd.apple.pkpass file. Android returns JSON containing a short-lived signed saveUrl. This operation does not return the Apple .p12, its password, or Google service-account material. Generation is safe to retry and does not prove installation on a phone.","operationId":"packageForPhone","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Platform-specific install artifact","content":{"application/vnd.apple.pkpass":{},"application/json":{"schema":{"$ref":"#/components/schemas/AndroidWalletPackage"}}}},"404":{"description":"Wallet certificate not found","content":{"application/vnd.apple.pkpass":{"schema":{"type":"array","items":{"type":"string","format":"byte"}}},"application/json":{"schema":{"type":"array","items":{"type":"string","format":"byte"}}}}},"409":{"description":"Wallet certificate is inactive or expired","content":{"application/vnd.apple.pkpass":{"schema":{"type":"array","items":{"type":"string","format":"byte"}}},"application/json":{"schema":{"type":"array","items":{"type":"string","format":"byte"}}}}},"503":{"description":"Platform signing configuration or logo retrieval is unavailable","content":{"application/vnd.apple.pkpass":{"schema":{"type":"array","items":{"type":"string","format":"byte"}}},"application/json":{"schema":{"type":"array","items":{"type":"string","format":"byte"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/users":{"get":{"tags":["Users"],"summary":"List users","description":"Returns a list of all users associated with the current tenant.\nThis endpoint is typically used in admin dashboards or management panels.\n","operationId":"getAllUsers","responses":{"200":{"description":"List of users returned successfully","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}},"example":[{"id":"u_001","email":"jane@solvotix.org","name":"Jane Doe"},{"id":"u_002","email":"john@solvotix.org","name":"John Smith"}]}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Users"],"summary":"Invite a new user","description":"Creates and invites a new user to the tenant via email.\nThe user receives an activation link generated by Firebase Authentication.\n\n**Workflow:**\n1. A tenant admin calls this endpoint with an email and name.\n2. Firebase creates the user and generates a password-setup link.\n3. Solvotix sends a branded invitation email to the new user.\n4. The new user joins the tenant upon activation.\n","operationId":"addUser","requestBody":{"description":"Payload with name and email for the new user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"},"example":{"email":"new@solvotix.org","name":"New User"}}},"required":true},"responses":{"200":{"description":"User invited successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"},"example":{"id":"u_010","email":"new@solvotix.org","name":"New User"}}}},"400":{"description":"Invalid request payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}},"security":[{"bearerAuth":[]}]}},"/api/thermostat/{id}/wifi":{"post":{"tags":["Thermostat"],"summary":"Set thermostat Wi-Fi credentials","description":"Queues wall thermostat operation subaction 1 (set Wi-Fi) using ssid and password.","operationId":"setWifi","parameters":[{"name":"id","in":"path","description":"Wall thermostat sensor ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThermostatWifiRequest"}}},"required":true},"responses":{"200":{"description":"Wi-Fi command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid thermostat or payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Thermostat not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/thermostat/{id}/target-temperature":{"post":{"tags":["Thermostat"],"summary":"Set target temperature","description":"Queues wall thermostat operation subaction 2 (set target temperature) using a decimal temperature with two decimals.","operationId":"setTargetTemperature","parameters":[{"name":"id","in":"path","description":"Wall thermostat sensor ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThermostatTargetTemperatureRequest"}}},"required":true},"responses":{"200":{"description":"Target temperature command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid thermostat or payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Thermostat not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/thermostat/{id}/restart":{"post":{"tags":["Thermostat"],"summary":"Restart thermostat","description":"Queues wall thermostat operation subaction 5 (restart).","operationId":"restart","parameters":[{"name":"id","in":"path","description":"Wall thermostat sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Restart command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid thermostat","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Thermostat not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/thermostat/{id}/receive-debug-data":{"post":{"tags":["Thermostat"],"summary":"Receive thermostat debug data","description":"Queues wall thermostat operation subaction 7 (receive debug data). The thermostat is expected to open a TCP connection to port 30100 and stream terminal output.","operationId":"receiveDebugData","parameters":[{"name":"id","in":"path","description":"Wall thermostat sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Receive debug data command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid thermostat","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Thermostat not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/thermostat/{id}/on":{"post":{"tags":["Thermostat"],"summary":"Turn thermostat on","description":"Queues wall thermostat operation subaction 3 (turn on).","operationId":"turnOn","parameters":[{"name":"id","in":"path","description":"Wall thermostat sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Turn on command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid thermostat","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Thermostat not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/thermostat/{id}/off":{"post":{"tags":["Thermostat"],"summary":"Turn thermostat off","description":"Queues wall thermostat operation subaction 4 (turn off).","operationId":"turnOff","parameters":[{"name":"id","in":"path","description":"Wall thermostat sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Turn off command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid thermostat","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Thermostat not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/thermostat/{id}/delete-wifi":{"post":{"tags":["Thermostat"],"summary":"Delete thermostat Wi-Fi credentials","description":"Queues wall thermostat operation subaction 6 (delete Wi-Fi).","operationId":"deleteWifi","parameters":[{"name":"id","in":"path","description":"Wall thermostat sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Delete Wi-Fi command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid thermostat","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Thermostat not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/pulse-open":{"post":{"tags":["Smart Locks"],"summary":"Pulse open a smart lock","description":"Triggers a short open pulse on the smart lock relay.","operationId":"pulseOpen","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Pulse open command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid sensor or payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/open":{"post":{"tags":["Smart Locks"],"summary":"Open a smart lock","description":"Sends a persistent open command to the smart lock.","operationId":"open","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Open command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid sensor or payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/codes/resend":{"post":{"tags":["Smart Locks"],"summary":"Resend all access codes","description":"Deletes all codes on the lock and re-sends every stored access code.","operationId":"resendCodes","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Resend commands queued","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MessageFrame"}}}}},"400":{"description":"Invalid sensor","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MessageFrame"}}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MessageFrame"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/codes/remove":{"post":{"tags":["Smart Locks"],"summary":"Remove access codes","description":"Removes one or more access codes from the smart lock.","operationId":"removeCodes","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartLockCodesRequest"}}},"required":true},"responses":{"200":{"description":"Remove codes command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid sensor or payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/codes/add":{"post":{"tags":["Smart Locks"],"summary":"Add access codes","description":"Adds one or more access codes to the smart lock. Slot allocation is serialized per lock within the runner process so concurrent requests cannot select the same free slot.","operationId":"addCodes","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartLockCodesRequest"}}},"required":true},"responses":{"200":{"description":"Add codes command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid sensor or payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/close":{"post":{"tags":["Smart Locks"],"summary":"Close a smart lock","description":"Sends a close command to the smart lock.","operationId":"close","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Close command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid sensor or payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/{id}/restart":{"post":{"tags":["Sensors"],"summary":"Restart sensor","description":"Queues a restart command for a sensor using node action 35.","operationId":"restart_1","parameters":[{"name":"id","in":"path","description":"Sensor ID","required":true,"schema":{"type":"string"},"example":"sensor_849FAD3"}],"responses":{"200":{"description":"Restart successfully queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid request","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/{id}/restart-mode/{enabled}":{"post":{"tags":["Sensors"],"summary":"Toggle restart mode","description":"Queues restart mode toggle on a sensor (`true` = on, `false` = off) and logs a Solvotix event.","operationId":"toggleRestartMode","parameters":[{"name":"id","in":"path","description":"Sensor ID","required":true,"schema":{"type":"string"},"example":"sensor_849FAD3"},{"name":"enabled","in":"path","description":"Restart mode state (`true` to enable, `false` to disable)","required":true,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Restart mode toggle successfully queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid request","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/{id}/pairings/fetch":{"post":{"tags":["Sensors"],"summary":"Queue a pairing read from a sensor","description":"Asks the device to return its currently persisted paired device list.","operationId":"fetchPairings","parameters":[{"name":"id","in":"path","description":"Sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Pairing read queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid sensor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/{id}/claim/{name}":{"post":{"tags":["Sensors"],"summary":"Claim a sensor","description":"Claims a sensor under the current tenant using the sensor UUID string and a human-readable name.\nIf the sensor is already claimed by another tenant, the request is rejected.\n","operationId":"claimSensor","parameters":[{"name":"id","in":"path","description":"Sensor UUID (payload.getUUIDString())","required":true,"schema":{"type":"string"}},{"name":"name","in":"path","description":"Human-readable sensor name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sensor claimed successfully","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Missing sensor id or name","content":{"*/*":{"schema":{"type":"object"}}}},"409":{"description":"Sensor already claimed by another tenant","content":{"*/*":{"schema":{"type":"object"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/{id}/advprofile/{profile}":{"post":{"tags":["Sensors"],"summary":"Change Bluetooth advertisement profile","description":"Updates the Bluetooth Low Energy advertisement profile of a specific sensor.\n\nSupported profile IDs:\n0=UltraSave, 1=UltraSaveLR, 2=Save, 3=SaveLR, 4=Normal, 5=NormalLR,\n6=Responsive, 7=ResponsiveLR, 8=MidResponsive, 9=MidResponsiveLR.\n","operationId":"changeProfile","parameters":[{"name":"id","in":"path","description":"Sensor ID","required":true,"schema":{"type":"string"},"example":"sensor_849FAD3"},{"name":"profile","in":"path","description":"Profile ID (0=UltraSave, 1=UltraSaveLR, 2=Save, 3=SaveLR, 4=Normal, 5=NormalLR, 6=Responsive, 7=ResponsiveLR, 8=MidResponsive, 9=MidResponsiveLR)","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"Profile change successfully queued","content":{"*/*":{"schema":{"type":"string"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"type":"string"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/{id}/action":{"post":{"tags":["Sensors"],"summary":"Perform an action on a sensor","description":"Sends a control command to a specific sensor (for example: `test`, `restart_node`, or `update_node`).\n\nFor a relay, action `40` opens it until the requested CF pulse count has been reached.\nSupply the unsigned 32-bit pulse count as a decimal string in `value`; `0` cancels\nan active countdown and closes the relay.\n\nThe backend dispatches this action via the associated gateway using the Solvotix protocol.\n","operationId":"sensorAction","parameters":[{"name":"id","in":"path","description":"Unique ID of the sensor","required":true,"schema":{"type":"string"},"example":"sensor_849FAD3"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorActionRequest"}}},"required":true},"responses":{"200":{"description":"Action successfully queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid or unsupported action request","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/relay/{id}/pulse/seconds/{seconds}":{"post":{"tags":["Relay"],"summary":"Pulse relay in seconds","description":"Queues a relay pulse command with second duration.","operationId":"pulseSeconds","parameters":[{"name":"id","in":"path","description":"Relay ID","required":true,"schema":{"type":"string"}},{"name":"seconds","in":"path","description":"Pulse duration in seconds (0-256)","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"Pulse command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid relay or duration","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Relay not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/relay/{id}/pulse/ms/{milliseconds}":{"post":{"tags":["Relay"],"summary":"Pulse relay in milliseconds","description":"Queues a relay pulse command with millisecond duration.","operationId":"pulseMilliseconds","parameters":[{"name":"id","in":"path","description":"Relay ID","required":true,"schema":{"type":"string"}},{"name":"milliseconds","in":"path","description":"Pulse duration in milliseconds (0-256)","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"Pulse command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid relay or duration","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Relay not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/relay/{id}/pulse/minutes/{minutes}":{"post":{"tags":["Relay"],"summary":"Pulse relay in minutes","description":"Queues a relay pulse command with minute duration.","operationId":"pulseMinutes","parameters":[{"name":"id","in":"path","description":"Relay ID","required":true,"schema":{"type":"string"}},{"name":"minutes","in":"path","description":"Pulse duration in minutes (0-256)","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"Pulse command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid relay or duration","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Relay not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/relay/{id}/pulse-open":{"post":{"tags":["Relay"],"summary":"Pulse open relay (auto unit)","description":"Opens relay based on `relay.configuration.relay_open_ms` and automatically chooses\nms/seconds/minutes command. Defaults to 5000ms (5 seconds) when not configured.\n","operationId":"pulseOpen_1","parameters":[{"name":"id","in":"path","description":"Relay ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Pulse command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid relay or duration","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Relay not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/relay/{id}/package":{"post":{"tags":["Relay"],"summary":"Create a relay operation package","description":"Returns a complete node-core data package for direct delivery to the relay without\nsaving or queueing it through a gateway. Supported operations are `open`, `close`,\n`pulse`, and `consumption`. Pulse requires `value` and `unit`; consumption requires\nan energy allowance in `kwh`. The backend converts kWh to a whole CF pulse count using\nthe BL0937B relay calibration (2,175,856 CF pulses/kWh by default) and returns that\ncount in the response.\n","operationId":"operationPackage","parameters":[{"name":"id","in":"path","description":"Relay ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RelayOperationPackageRequest"}}}},"responses":{"200":{"description":"Relay package returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RelayOperationPackage"}}}},"400":{"description":"Invalid relay operation or value","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RelayOperationPackage"}}}},"404":{"description":"Relay not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RelayOperationPackage"}}}}},"security":[{"bearerAuth":[]}]}},"/api/relay/{id}/open":{"post":{"tags":["Relay"],"summary":"Open relay using configured milliseconds","description":"Uses the relay configuration key `relay_open_ms` and sends a millisecond pulse command.","operationId":"open_1","parameters":[{"name":"id","in":"path","description":"Relay ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Open command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid relay","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Relay not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/relay/{id}/open-until-closed":{"post":{"tags":["Relay"],"summary":"Open relay until closed","description":"Queues relay open action (without pulse duration).","operationId":"openUntilClosed","parameters":[{"name":"id","in":"path","description":"Relay ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Open until closed command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid relay","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Relay not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/relay/{id}/close":{"post":{"tags":["Relay"],"summary":"Close relay","description":"Queues relay close action.","operationId":"close_1","parameters":[{"name":"id","in":"path","description":"Relay ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Close command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid relay","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Relay not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/node-updates/sensors/{sensorId}":{"post":{"tags":["Node Updates"],"summary":"Start a node firmware update","description":"Queues a tenant-scoped OTA update. For a firmware-111 node known to hold a trust anchor, the backend loads the adjacent offline-signed manifest, signature, and DER signer certificate from /firmware. It verifies that the manifest binds the selected image, filename version, image size, target node type and SHA-256 digest, validates the per-release signer certificate against the backend intermediate, and verifies the RSA-2048/SHA-256 signature. It then sends those exact signed bytes, the signer certificate, and the intermediate to the node. The backend does not load or require an OTA private key or a global flash-signing certificate. Missing or invalid bundle files or public verification material stop the update instead of falling back to unsigned OTA. Rootless recovery nodes retain the legacy unsigned path. Upgrading a smart lock from firmware below 111 to 111 or newer migrates code slots 1-987 automatically; codes in slots 988-1980 are not retained and must be pushed again. Downgrading a smart lock from firmware 111 or newer to a version below 111 makes the legacy firmware treat the version-6 code store as empty, so every code must be pushed again. Saving codes on that legacy firmware can overwrite the firmware-111 certificate and session storage, and communication continues through the legacy plaintext protocol. A secure node keeps its active session throughout authenticated downgrade delivery; after the node cryptographically acknowledges the exact secure finalize frame, or after positive finalize transport delivery when it advertises the explicitly authorized target firmware, the backend retires the old session. A gateway status-2 receipt or advertisement flag alone never authorizes a downgrade. If the sensor already has an active OTA session or pending OTA packages, returns 409 without replacing, cancelling, or modifying that transfer.","operationId":"startUpdate","parameters":[{"name":"sensorId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeUpdateRequest"}}}},"responses":{"202":{"description":"Node update queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/NodeUpdateStartResponse"}}}},"400":{"description":"Invalid request","content":{"*/*":{"schema":{"$ref":"#/components/schemas/NodeUpdateStartResponse"}}}},"409":{"description":"Update already running","content":{"*/*":{"schema":{"$ref":"#/components/schemas/NodeUpdateStartResponse"}}}},"500":{"description":"Firmware or signed bundle metadata could not be loaded","content":{"*/*":{"schema":{"$ref":"#/components/schemas/NodeUpdateStartResponse"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Node Updates"],"summary":"Abort a node firmware update","description":"The authoritative tenant-scoped OTA cancellation operation. Always removes any in-memory and persisted OTA queue entries for the sensor, cancels its OTA session, clears chunk tracking, and resets update progress. The operation is idempotent and returns 200 with zero when no OTA state remains.","operationId":"abortUpdate","parameters":[{"name":"sensorId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Update aborted, returns number of removed messages","content":{"*/*":{"schema":{"type":"integer","format":"int32"}}}},"400":{"description":"Invalid sensor ID","content":{"*/*":{"schema":{"type":"integer","format":"int32"}}}}},"security":[{"bearerAuth":[]}]}},"/api/lockusers/{id}/wallet-certificates":{"post":{"tags":["Lock Users"],"summary":"Assign wallet certificate to lock user","description":"Stores the assignment and queues the certificate credential to every compatible lock sensor assigned to the lock user. Repeating the same assignment does not duplicate it.","operationId":"assignWalletCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificateAssignmentRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/lock-users/{id}/wallet-certificates":{"post":{"tags":["Lock Users"],"summary":"Assign wallet certificate to lock user","description":"Stores the assignment and queues the certificate credential to every compatible lock sensor assigned to the lock user. Repeating the same assignment does not duplicate it.","operationId":"assignWalletCertificate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificateAssignmentRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/lockusers/{id}/mifare-credentials":{"post":{"tags":["Lock Users"],"summary":"Add MIFARE credential to lock user","description":"Adds one MIFARE card credential, with an optional PIN, to a lock user and queues it for every assigned lock sensor that supports the extended credential store (device types lock_8015 and lock_s42). Sensors of other types are skipped. Adding a UID that the lock user already has replaces its PIN instead of creating a second entry, so the call is safe to retry. The credential is queued, not confirmed: poll the lock user and read mifareCredentialsNotUploadedToAllSensors, or GET /api/smart-locks/{id}/codes/slots, to see when the lock has stored it. In the web app this is the Access / Lock users page, where the card is listed under the lock user's credentials.","operationId":"addMifareCredential","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUserMifareCredential"}}},"required":true},"responses":{"200":{"description":"Credential stored and queued for the lock user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"400":{"description":"MIFARE UID is not 4, 7, or 10 bytes of hex, or the code is not 4-7 digits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"404":{"description":"Lock user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/lock-users/{id}/mifare-credentials":{"post":{"tags":["Lock Users"],"summary":"Add MIFARE credential to lock user","description":"Adds one MIFARE card credential, with an optional PIN, to a lock user and queues it for every assigned lock sensor that supports the extended credential store (device types lock_8015 and lock_s42). Sensors of other types are skipped. Adding a UID that the lock user already has replaces its PIN instead of creating a second entry, so the call is safe to retry. The credential is queued, not confirmed: poll the lock user and read mifareCredentialsNotUploadedToAllSensors, or GET /api/smart-locks/{id}/codes/slots, to see when the lock has stored it. In the web app this is the Access / Lock users page, where the card is listed under the lock user's credentials.","operationId":"addMifareCredential_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUserMifareCredential"}}},"required":true},"responses":{"200":{"description":"Credential stored and queued for the lock user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"400":{"description":"MIFARE UID is not 4, 7, or 10 bytes of hex, or the code is not 4-7 digits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"404":{"description":"Lock user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/lock-users":{"get":{"tags":["Lock Users"],"summary":"List lock users","description":"Returns computed numeric-code upload status: codesNotUploadedToAllSensors counts pending lock/code pairs; sensorIdsWithCodesNotUploaded lists the distinct affected code-capable sensor IDs. Confirmation requires ADDED_TO_LOCK state and uploadedToLockAt. Credential masking does not affect the device IDs. Reading does not queue commands.","operationId":"list_1","responses":{"200":{"description":"List of lock users","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LockUser"}}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Lock Users"],"summary":"Create lock user","description":"Creates a lock user and queues its codes for all assigned lock sensors.","operationId":"create_1","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}},"required":true},"responses":{"200":{"description":"Lock user created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"400":{"description":"Invalid payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/lockusers":{"get":{"tags":["Lock Users"],"summary":"List lock users","description":"Returns computed numeric-code upload status: codesNotUploadedToAllSensors counts pending lock/code pairs; sensorIdsWithCodesNotUploaded lists the distinct affected code-capable sensor IDs. Confirmation requires ADDED_TO_LOCK state and uploadedToLockAt. Credential masking does not affect the device IDs. Reading does not queue commands.","operationId":"list_2","responses":{"200":{"description":"List of lock users","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LockUser"}}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Lock Users"],"summary":"Create lock user","description":"Creates a lock user and queues its codes for all assigned lock sensors.","operationId":"create_2","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}},"required":true},"responses":{"200":{"description":"Lock user created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"400":{"description":"Invalid payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/guest-portal/bookings/{bookingId}/sensors/{sensorId}/action":{"post":{"tags":["Guest Portal"],"summary":"Perform an action on a sensor for a guest booking","operationId":"sensoraction","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}},{"name":"sensorId","in":"path","description":"Sensor ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorActionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}}}},"/api/guest-portal/bookings/{bookingId}/checkout":{"post":{"tags":["Guest Portal"],"summary":"Check out a guest and request source-provider checkout","description":"Booking-scoped Guest Portal operation with tenantId query context and no request body. Requires a known booking and room, an enabled checkout button, and a valid booking window. HTTP 200 confirms local checkout, sets the stored booking end five minutes after the request, and reports the result of marking the room dirty. Physical credential removal follows the existing code-lifecycle and delivery rules. For supported source providers, a previously checked-in booking also receives an immediate durable checkout request, independently of the opt-in checkout_after_end booking filter. The lifecycle monitor retries transient provider failures. Stale inbound snapshots cannot reopen the guest's checked-out booking. Local success or a booking event with data.state=checked_out does not prove provider completion; verify in the source provider. Requires explicit guest intent because it ends access. Do not repeat after success; repeated checkout returns 403. Unknown booking or room returns 404; disabled checkout, missing room assignment, or invalid booking window returns 403.","operationId":"checkout","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestCheckoutResponse"}}}}}}},"/api/guest-portal/bookings/{bookingId}/ai/threads":{"get":{"tags":["Guest Portal"],"summary":"List AI chat threads for a booking","operationId":"listAIThreads","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AIChatThread"}}}}}}},"post":{"tags":["Guest Portal"],"summary":"Start AI chat thread for a booking","operationId":"startAIThread","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIChatThread"}}}}}}},"/api/guest-portal-admin/direction-routes":{"get":{"tags":["Guest Portal Admin"],"summary":"List guest direction routes","operationId":"listDirectionRoutes","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GuestPortalDirectionRoute"}}}}}}},"post":{"tags":["Guest Portal Admin"],"summary":"Create a guest direction route","operationId":"createDirectionRoute","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalDirectionRoute"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalDirectionRoute"}}}}}}},"/api/guest-portal-admin/direction-images":{"get":{"tags":["Guest Portal Admin"],"summary":"List guest direction image metadata","operationId":"listDirectionImages","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GuestPortalDirectionImage"}}}}}}},"post":{"tags":["Guest Portal Admin"],"summary":"Upload a Base64 guest direction image","operationId":"createDirectionImage","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalDirectionImage"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalDirectionImage"}}}}}}},"/api/gateways/{id}/{name}":{"post":{"tags":["Gateways"],"summary":"Claim a new gateway","description":"Registers a new gateway with a specified name under the current user's account.","operationId":"claimGateway","parameters":[{"name":"id","in":"path","description":"Gateway ID","required":true,"schema":{"type":"string"}},{"name":"name","in":"path","description":"Human-readable gateway name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gateway"}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{id}/replacement-request":{"post":{"tags":["Gateways"],"summary":"Request a replacement for a lost gateway","description":"From Gateways, open the offline gateway's diagnostics, select The gateway is\nlost, then choose Yes. This operation sends one email to Solvotix support\nwith the subject New gateway request and the authenticated tenant name,\ngateway name, gateway description, and gateway ID. The server obtains\nthese values from stored tenant and gateway records and the verified URL\nidentifier; clients cannot supply or override the email fields. The operation does\nnot delete the gateway, place an order,\nor confirm delivery to the recipient. A 204 means the mail service accepted\nthe message. Gateway removal is a separate action to verify afterward.\n\nRequires bearer authentication and the Tenant header. No request body.\nThis operation is not idempotent: do not automatically retry an ambiguous\nresult, because another email may be sent. Missing gateway or tenant\nreturns 404. A mail transport failure returns 5xx and leaves the gateway\nregistration unchanged.\n","operationId":"requestGatewayReplacement","parameters":[{"name":"id","in":"path","description":"ID of the lost gateway in the authenticated tenant","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Replacement request email accepted by mail service"},"404":{"description":"Gateway or tenant not found"},"500":{"description":"Mail service failed; gateway not removed"}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{gatewayId}/relay/restart":{"post":{"tags":["Gateways"],"summary":"Restart relay","description":"Commands the gateway to toggle the relay for ~200ms (status 25).","operationId":"restartRelay","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Command accepted"},"404":{"description":"Gateway not found"}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{gatewayId}/relay/on":{"post":{"tags":["Gateways"],"summary":"Turn relay on","description":"Instructs the gateway to close the onboard relay (status 23).","operationId":"turnRelayOn","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Command accepted"},"404":{"description":"Gateway not found"}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{gatewayId}/relay/off":{"post":{"tags":["Gateways"],"summary":"Turn relay off","description":"Instructs the gateway to open the onboard relay (status 24).","operationId":"turnRelayOff","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Command accepted"},"404":{"description":"Gateway not found"}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{gatewayId}/receive-debug-data":{"post":{"tags":["Gateways"],"summary":"Receive gateway debug data","description":"Instructs the gateway to open a TCP connection to port 30100 and stream terminal output (status 29).","operationId":"receiveDebugData_1","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Command accepted"},"404":{"description":"Gateway not found"}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{gatewayId}/nodes/{nodeId}/connection/open":{"post":{"tags":["Gateways"],"summary":"Open a connection to a node","description":"Instructs the gateway to open a BLE session for the provided node. The backend will then wait for the gateway to report status=18/20.","operationId":"openConnection","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID managing the node","required":true,"schema":{"type":"string"}},{"name":"nodeId","in":"path","description":"Node UUID that should be connected","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{gatewayId}/nodes/{nodeId}/connection/close":{"post":{"tags":["Gateways"],"summary":"Close an active node connection","description":"Tells the gateway to drop any concurrent connection for the given node (status=19).","operationId":"closeConnection","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID managing the node","required":true,"schema":{"type":"string"}},{"name":"nodeId","in":"path","description":"Node UUID whose connection should be closed","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"bearerAuth":[]}]}},"/api/events":{"get":{"tags":["Events"],"summary":"Get all events","description":"Retrieves a list of all Solvotix events in the system.\nThis endpoint returns both active and historical events, typically used by dashboards or analytics.\nThe bearer token and required `Tenant` header supply tenant context. See `SolvotixEvent.action`, `sub_action`, and `data.state`\nfor the complete device-protocol and application-event registry.\n","operationId":"getAllEvents","responses":{"200":{"description":"List of all events retrieved successfully","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SolvotixEvent"}}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Events"],"summary":"Create a new event","description":"Creates and stores a new event in the Solvotix event database.\nThe request body must contain a valid `Event` object with required metadata.\n","operationId":"create_3","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"}}},"required":true},"responses":{"200":{"description":"Event successfully created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"}}}},"400":{"description":"Invalid event data provided"}},"security":[{"bearerAuth":[]}]}},"/api/cleaning/rooms/{roomId}/cleaned":{"post":{"tags":["Cleaning"],"summary":"Mark room as cleaned","operationId":"markRoomAsCleaned","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Room marked as cleaned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleaningRoom"}}}},"404":{"description":"Room not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleaningRoom"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms":{"get":{"tags":["Bookings"],"summary":"List rooms","operationId":"listRooms_1","responses":{"200":{"description":"List of rooms","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Rooms"}}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Bookings"],"summary":"Create room","operationId":"createRoom","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}},"required":true},"responses":{"200":{"description":"Created room","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms/{roomId}/wallet-certificates":{"get":{"tags":["Bookings"],"summary":"List wallet certificates assigned to room","description":"Returns the persisted wallet certificate IDs whose credentials apply to the compatible locks in the room.","operationId":"getRoomWalletCertificates","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomWalletCertificates"}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Bookings"],"summary":"Assign wallet certificate to room","description":"Stores the assignment on the room and queues the wallet credential to every compatible lock in room.sensorIds. Repeating the same assignment does not duplicate it.","operationId":"assignRoomWalletCertificate","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletCertificateAssignmentRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomWalletCertificates"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms/{roomId}/codes":{"post":{"tags":["Bookings"],"summary":"Add room codes","description":"Adds one or more explicit 4-7 digit codes to a room and ensures they are present on all room locks.","operationId":"addRoomCodes","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartLockCodesRequest"}}},"required":true},"responses":{"200":{"description":"Updated room","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}}},"400":{"description":"Invalid code payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}}},"404":{"description":"Room not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}}},"409":{"description":"Code already exists in another room","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms/{roomId}/codes/{code}/refresh":{"post":{"tags":["Bookings"],"summary":"Refresh room code","description":"Replaces the given room code with a new random code using the tenant's roomCodeLength setting (4-7 digits; default 4). Locks with existing codes of another length reject the new code.","operationId":"refreshRoomCode","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}},{"name":"code","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"New code returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeResponse"}}}},"404":{"description":"Room not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeResponse"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms/{roomId}/codes/random":{"post":{"tags":["Bookings"],"summary":"Add random room code","description":"Generates a random code using the tenant's roomCodeLength setting (4-7 digits; default 4) and adds it to the room. Codes are unique across all rooms. Locks with existing codes of another length reject the new code.","operationId":"addRandomRoomCode","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated room","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}}},"404":{"description":"Room not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms/codes/resync":{"post":{"tags":["Bookings"],"summary":"Resync all room codes to room locks","description":"Queues every valid stored room code to every code-capable lock assigned to each room.","operationId":"resyncAllRoomCodes","responses":{"200":{"description":"Resync commands queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeResyncResponse"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/bookings":{"get":{"tags":["Bookings"],"summary":"List bookings","description":"Returns all bookings with optional filters for check-in/out status and date range. Each result includes response-only automated-message delivery and current block status.","operationId":"listBookings","parameters":[{"name":"checkedIn","in":"query","description":"Filter by checked-in state","required":false,"schema":{"type":"boolean"}},{"name":"checkedOut","in":"query","description":"Filter by checked-out state","required":false,"schema":{"type":"boolean"}},{"name":"startDate","in":"query","description":"Filter start date (inclusive, yyyy-MM-dd)","required":false,"schema":{"type":"string","format":"date"}},{"name":"endDate","in":"query","description":"Filter end date (inclusive, yyyy-MM-dd)","required":false,"schema":{"type":"string","format":"date"}},{"name":"startingToday","in":"query","description":"Only bookings starting today","required":false,"schema":{"type":"boolean","default":false}},{"name":"activeToday","in":"query","description":"Only bookings that have started and not yet ended","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"List of bookings","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Booking"}}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Bookings"],"summary":"Create booking","description":"Creates a new booking.","operationId":"createBooking","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}},"required":true},"responses":{"200":{"description":"Created booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"description":"Invalid payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"409":{"description":"Booking already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/bookings/{bookingId}/send-message":{"post":{"tags":["Bookings"],"summary":"Send automated message to booking guest","description":"Triggers the configured automated message for a booking with optional contact overrides. Templates support {departureDate} for the booking end date in the tenant timezone and regional numeric format (Europe/Oslo: 18.09.2026; America/New_York: 09/18/2026), and {departureDateText} for a lowercase English date such as 18 sep 2026. Both work in the subject, general, email and SMS bodies, including inserted room instructions. Missing booking dates produce an empty value. Missing or invalid tenant timezones use UTC; timezones without a supported country convention use yyyy-MM-dd. The booking language selects room templates but does not change date formatting. {arrivalDate} and {arrivalDateText} format the booking start with the same numeric and text rules. Use {guestportalbutton=\"Open guest portal\", color=\"#1068F0\", textColor=\"#F8F8F8\"} as a standalone body placeholder (not inside an HTML tag or link). Only email bodies render a clickable button; SMS and subjects leave this token unchanged. Use {guestportal} there. {guestportalbutton} defaults to label Open guest portal. Omitted or invalid colors default to Solvotix action blue #1068F0; color accepts #RGB or #RRGGBB. Text defaults to #F8F8F8, with #001838 on light custom backgrounds. Optional textColor overrides the text using #RGB or #RRGGBB; omitted or invalid textColor retains automatic text color. When both are supplied, color precedes textColor. Labels are HTML-escaped. Requires a booking ID and tenant context; otherwise the placeholder remains unchanged. Supported in email bodies, including general templates used as email fallback, inserted room instructions and manual email overrides. Existing {guestportal} remains a plain URL. ","operationId":"sendBookingMessage","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingMessageRequest"}}}},"responses":{"202":{"description":"Message queued"},"400":{"description":"No applicable trigger or contact details"},"404":{"description":"Booking not found"}},"security":[{"bearerAuth":[]}]}},"/api/bookings/bookings/{bookingId}/message-preview":{"post":{"tags":["Bookings"],"summary":"Preview automated message for booking guest","description":"Returns the rendered automated message (email/SMS) for confirmation before sending. Templates support {departureDate} for the booking end date in the tenant timezone and regional numeric format (Europe/Oslo: 18.09.2026; America/New_York: 09/18/2026), and {departureDateText} for a lowercase English date such as 18 sep 2026. Both work in the subject, general, email and SMS bodies, including inserted room instructions. Missing booking dates produce an empty value. Missing or invalid tenant timezones use UTC; timezones without a supported country convention use yyyy-MM-dd. The booking language selects room templates but does not change date formatting. {arrivalDate} and {arrivalDateText} format the booking start with the same numeric and text rules. Use {guestportalbutton=\"Open guest portal\", color=\"#1068F0\", textColor=\"#F8F8F8\"} as a standalone body placeholder (not inside an HTML tag or link). Only email bodies render a clickable button; SMS and subjects leave this token unchanged. Use {guestportal} there. {guestportalbutton} defaults to label Open guest portal. Omitted or invalid colors default to Solvotix action blue #1068F0; color accepts #RGB or #RRGGBB. Text defaults to #F8F8F8, with #001838 on light custom backgrounds. Optional textColor overrides the text using #RGB or #RRGGBB; omitted or invalid textColor retains automatic text color. When both are supplied, color precedes textColor. Labels are HTML-escaped. Requires a booking ID and tenant context; otherwise the placeholder remains unchanged. Supported in email bodies, including general templates used as email fallback, inserted room instructions and manual email overrides. Existing {guestportal} remains a plain URL. ","operationId":"previewBookingMessage","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingMessageRequest"}}}},"responses":{"200":{"description":"Preview status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingMessagePreviewResponse"}}}},"404":{"description":"Booking not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingMessagePreviewResponse"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/bookings/{bookingId}/code":{"post":{"tags":["Bookings"],"summary":"Set booking room code","description":"Assigns a specific code to the booking. If the code already exists on the booking room it is reused; otherwise it is added to the room first. A successful response does not prove guest-message delivery or publication to the booking source system. Verify guest delivery and source-system attachment separately.","operationId":"setCode","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingRoomCodeRequest"}}},"required":true},"responses":{"200":{"description":"Updated booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"description":"Booking cannot be assigned the room code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"description":"Booking not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"409":{"description":"Code already belongs to another room or booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/bookings/{bookingId}/code/refresh":{"post":{"tags":["Bookings"],"summary":"Refresh booking room code","description":"Picks a room code for the booking and stores it on the booking. A successful response does not prove guest-message delivery or publication to the booking source system. Verify guest delivery and source-system attachment separately.","operationId":"refreshCode","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"description":"Booking cannot be assigned a room code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"description":"Booking not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}}},"security":[{"bearerAuth":[]}]}},"/api/automation/messages/triggers":{"get":{"tags":["Automated Messages"],"summary":"List configured triggers","description":"Lists all configured automation triggers for the current tenant.","operationId":"listTriggers","responses":{"200":{"description":"List of triggers","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AutomationTrigger"}}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Automated Messages"],"summary":"Create trigger","description":"Create an automated message trigger from the supported trigger list. Templates support {departureDate} for the booking end date in the tenant timezone and regional numeric format (Europe/Oslo: 18.09.2026; America/New_York: 09/18/2026), and {departureDateText} for a lowercase English date such as 18 sep 2026. Both work in the subject, general, email and SMS bodies, including inserted room instructions. Missing booking dates produce an empty value. Missing or invalid tenant timezones use UTC; timezones without a supported country convention use yyyy-MM-dd. The booking language selects room templates but does not change date formatting. {arrivalDate} and {arrivalDateText} format the booking start with the same numeric and text rules.  Use {guestportalbutton=\"Open guest portal\", color=\"#1068F0\", textColor=\"#F8F8F8\"} as a standalone body placeholder (not inside an HTML tag or link). Only email bodies render a clickable button; SMS and subjects leave this token unchanged. Use {guestportal} there. {guestportalbutton} defaults to label Open guest portal. Omitted or invalid colors default to Solvotix action blue #1068F0; color accepts #RGB or #RRGGBB. Text defaults to #F8F8F8, with #001838 on light custom backgrounds. Optional textColor overrides the text using #RGB or #RRGGBB; omitted or invalid textColor retains automatic text color. When both are supplied, color precedes textColor. Labels are HTML-escaped. Requires a booking ID and tenant context; otherwise the placeholder remains unchanged. Supported in email bodies, including general templates used as email fallback, inserted room instructions and manual email overrides. Existing {guestportal} remains a plain URL. For booking-guest triggers, sendOnlyWhenRoomClean defers delivery until the assigned room is marked clean. For booking-guest triggers, onlySendIfPaidOrCheckedIn defers delivery until the booking is paid in full (amountPaid at least totalAmount minus a tolerance of 2, and totalAmount above zero) or the guest is checked in. Both flags require sendToBookingGuest and default to false. Deferred deliveries are re-evaluated about once per minute and expire with the trigger's own eligibility window. when_start_time_has_passed becomes eligible at booking start plus offsetMinutes, including on import/update after that threshold. offsetMinutes is an optional integer: -60 sends one hour earlier, 60 one hour later; omitted or null stores 0. It is ignored for other trigger types. For example, {\"triggerKey\":\"when_start_time_has_passed\",\"sendToBookingGuest\":true,\"sendEmail\":true,\"offsetMinutes\":-60,\"emailMessage\":\"Welcome!\"}. This also supports delivery for late-created active reservations. Booking creation time and message-rule modification time do not exclude an active stay; delivered channels are deduplicated for the existing trigger and booking code. Editing wording does not resend a delivered message. This trigger stops at booking end or checkout. Creating a new trigger can send to active bookings that have not received that trigger, so confirm the intended audience first. Authenticate with a bearer token and tenant context supplied through the Tenant header. HTTP 200 returns the saved configuration, not proof of delivery. Read GET /api/automation/messages/triggers before retrying an ambiguous create; a duplicate trigger can send again. Verify successful channels using GET /api/automation/messages/logs/bookings/{bookingId}. No new event or queue payload is introduced.","operationId":"createTrigger","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationTriggerRequest"}}},"required":true},"responses":{"200":{"description":"Trigger created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationTrigger"}}}},"400":{"description":"Invalid trigger request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationTrigger"}}}}},"security":[{"bearerAuth":[]}]}},"/api/automation/locks/tasks":{"get":{"tags":["Lock Automation"],"summary":"List scheduled lock automation tasks","description":"Lists every scheduled lock automation task for the current tenant, newest first.\nDisabled tasks are included; check `enabled` to tell them apart.\n\n`lastFiredOccurrence` holds the local date-time of the most recently executed\nscheduled occurrence. `lastFiredAt` holds the server time of the most recent\nscheduled or manual dispatch attempt; it does not confirm physical device action.\n","operationId":"listTasks","responses":{"200":{"description":"Tasks for the current tenant, newest first; empty when none exist","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LockAutomationTask"}}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LockAutomationTask"}}}}},"403":{"description":"Superuser access required","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LockAutomationTask"}}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Lock Automation"],"summary":"Create a scheduled lock automation task","description":"Creates a task and starts scheduling it immediately. The creating user and creation\ntime are recorded on the task as `userId` and `createdDate`.\n\nEvery id in `deviceIds` must resolve to an existing lock, lock controller or relay in\nthe tenant; the whole request is rejected if any of them does not. `enabled` defaults\nto true when omitted.\n\nExample — unlock the main entrance every weekday at 08:00:\n\n```json\n{\n  \"name\": \"Unlock main entrance\",\n  \"comment\": \"Opens the front door for the morning shift\",\n  \"trigger\": \"open\",\n  \"deviceIds\": [\"A1B2C3D4E5F60708\"],\n  \"schedule\": {\n    \"frequency\": \"WEEKLY\",\n    \"interval\": 1,\n    \"byWeekdays\": [\"MON\", \"TUE\", \"WED\", \"THU\", \"FRI\"],\n    \"timeOfDay\": \"08:00\",\n    \"startDate\": \"2026-09-01\",\n    \"timeZone\": \"Europe/Oslo\"\n  }\n}\n```\n\nCreating a task is not idempotent: a retry after an ambiguous response creates a\nsecond task. Confirm with `GET /api/automation/locks/tasks` before retrying.\n","operationId":"createTask","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationTaskRequest"}}},"required":true},"responses":{"200":{"description":"Task created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationTask"}}}},"400":{"description":"Invalid task, schedule, trigger or device list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationTask"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationTask"}}}},"403":{"description":"Superuser access required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationTask"}}}}},"security":[{"bearerAuth":[]}]}},"/api/automation/locks/tasks/{id}/run":{"post":{"tags":["Lock Automation"],"summary":"Run a lock automation task now","description":"Immediately queues the task's configured `trigger` for each device in its `deviceIds`\nlist, without waiting for its schedule. This is available from **Automation → Lock\nAutomation → task action menu → Run now**. The authenticated superuser and `Tenant`\nheader select the tenant; the task must belong to that tenant, be enabled, and have\nthe Lock Automation module enabled under **Settings → Modules**. No request body is\nused. `lastFiredAt` is updated to the manual run's `requestedAt` when dispatch\nstarts, even if no device ultimately queues successfully. `lastFiredOccurrence`\nremains unchanged, and a scheduled occurrence can still run around the same time.\n\nThe response reports each device's queue result and a unique `runId` that appears\nin the per-device executed or failed events as `manual:<runId>`. A `queued` result\nmeans only that the command was accepted into the server/gateway queue; it does not\nconfirm the lock or relay acted. Check `/api/events` for each device and verify its\nphysical state before concluding that the run succeeded. Device dispatches are\nindependent, so some may queue while others fail. This operation changes physical\naccess and is not idempotent. Obtain explicit approval for the target task, trigger,\nand devices before calling. Never automatically retry after a timeout or ambiguous\nresponse: inspect the event log and physical devices first to avoid duplicate\ncommands.\n","operationId":"runNow","parameters":[{"name":"id","in":"path","description":"ID of the lock automation task to dispatch now","required":true,"schema":{"type":"string"},"example":"8f14e45f-ceea-467a-9575-4a1f3b7c2d90"}],"responses":{"200":{"description":"Manual dispatch attempted; inspect every device result. Queued does not mean physically completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationRunResult"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationRunResult"}}}},"403":{"description":"Superuser access required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationRunResult"}}}},"404":{"description":"Task not found in the authenticated tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationRunResult"}}}},"409":{"description":"Lock Automation module or task is disabled, or the task is not runnable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockAutomationRunResult"}}}}},"security":[{"bearerAuth":[]}]}},"/api/auth/session":{"get":{"tags":["Browser session"],"summary":"Check browser session","description":"Returns authenticated=true when the request has passed authentication.","operationId":"getSession","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"boolean"}}}}}}},"post":{"tags":["Browser session"],"summary":"Create a browser session","description":"Exchanges a Firebase ID token for an HttpOnly session cookie. Solvotix sat_ API tokens are not session-cookie credentials.","operationId":"createSession","parameters":[{"name":"Authorization","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Session cookie created"},"401":{"description":"Bearer token missing or invalid"}}},"delete":{"tags":["Browser session"],"summary":"Clear browser session","description":"Expires the browser session cookie.","operationId":"clearSession","responses":{"204":{"description":"Session cookie cleared"}}}},"/api/api-users":{"get":{"tags":["API users"],"summary":"List API users","description":"Lists machine identities and assigned roles for the selected tenant. Interactive users only; plaintext tokens are never returned.","operationId":"list_3","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApiUser"}}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["API users"],"summary":"Create an API user","description":"Creates a tenant-bound machine identity with optional roles. Empty roles preserve unrestricted access. The plaintext sat_ token is returned once in this response and cannot be retrieved later.","operationId":"create_4","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUser"}}},"required":true},"responses":{"201":{"description":"API user created; response includes the one-time plaintext token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUser"}}}},"400":{"description":"Tenant header or name is missing, or a supplied role is unknown","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUser"}}}},"403":{"description":"Caller is an API user or lacks tenant access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/api-users/{id}/rotate":{"post":{"tags":["API users"],"summary":"Rotate an API-user token","description":"Immediately invalidates the old token and returns the replacement plaintext token once. Assigned roles are preserved.","operationId":"rotate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Token rotated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUser"}}}},"403":{"description":"Caller is an API user or lacks tenant access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUser"}}}},"404":{"description":"API user not found in the selected tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/ai/threads/message":{"post":{"tags":["AI Chat Threads"],"summary":"Send message and get AI reply","description":"Stores the user message in the thread and returns the generated AI reply.","operationId":"sendMessage","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIChatReplyRequest"}}},"required":true},"responses":{"200":{"description":"Reply returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIChatReplyResponse"}}}},"400":{"description":"Missing session_id or text","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIChatReplyResponse"}}}},"404":{"description":"Thread not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIChatReplyResponse"}}}}},"security":[{"bearerAuth":[]}]}},"/api/users/testpushnotification/{fcmToken}":{"get":{"tags":["Users"],"summary":"Register a user for push notifications","operationId":"testPushNotification","parameters":[{"name":"fcmToken","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"bearerAuth":[]}]}},"/api/users/pushnotification/{tenantId}":{"get":{"tags":["Users"],"summary":"Register a user for push notifications","operationId":"getAllNotifications","parameters":[{"name":"tenantId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DevicePushRegistration"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/temperature-control/stats":{"get":{"tags":["Temperature Control"],"summary":"Get latest aggregate temperature stats","description":"Returns the most recent 5-minute aggregate snapshot for the current tenant.\n\nEvery five minutes the backend decodes the temperature of each sensor of type\n`temperature` or `wall_thermostat` from bytes 0-1 of its advertised `sensor_data`\n(signed 16-bit little-endian hundredths of a degree Celsius, `FFFF` meaning no reading)\nand stores the average, lowest and highest value in degrees Celsius. Sensors without a\nvalid reading are excluded, and no snapshot is written when no sensor has one.\nHumidity, which nodes report in byte 2 of the same `sensor_data`, is not aggregated or\nstored; read it per sensor from `GET /api/sensor`.\n","operationId":"getLatestStats","responses":{"200":{"description":"Latest stats returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemperatureStatsResponse"}}}},"400":{"description":"Missing tenant context","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemperatureStatsResponse"}}}}},"security":[{"bearerAuth":[]}]}},"/api/temperature-control/history/{sensorId}":{"get":{"tags":["Temperature Control"],"summary":"Get temperature history for a sensor","description":"Returns the collected 5-minute temperature readings for a specific sensor, each already\ndecoded to degrees Celsius from bytes 0-1 of the node's advertised `sensor_data`, so no\nclient-side decoding is required. Intervals where the node had no valid reading are\nsimply absent. Humidity is not part of the history; read the current value from byte 2 of\n`adv_data.sensor_data` on `GET /api/sensor`.\n","operationId":"getSensorHistory","parameters":[{"name":"sensorId","in":"path","description":"Sensor id","required":true,"schema":{"type":"string"},"example":"A1B2C3D4E5F60708"}],"responses":{"200":{"description":"Sensor temperature history returned successfully","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TemperatureSensorHistory"}}}}},"400":{"description":"Missing tenant context or sensor id","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TemperatureSensorHistory"}}}}},"404":{"description":"Sensor not found","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TemperatureSensorHistory"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/packages/pulse-open":{"get":{"tags":["Smart Locks"],"summary":"Download pulse-open package","description":"Returns the Bluetooth data package for the mobile app to forward directly to the smart lock.","operationId":"pulseOpenPackage","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Pulse-open package returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockOperationPackage"}}}},"400":{"description":"Invalid sensor","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockOperationPackage"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockOperationPackage"}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/packages/open":{"get":{"tags":["Smart Locks"],"summary":"Download open package","description":"Returns the Bluetooth data package for the mobile app to forward directly to the smart lock.","operationId":"openPackage","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Open package returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockOperationPackage"}}}},"400":{"description":"Invalid sensor","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockOperationPackage"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockOperationPackage"}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/packages/lock":{"get":{"tags":["Smart Locks"],"summary":"Download lock package","description":"Returns the Bluetooth data package for the mobile app to forward directly to the smart lock.","operationId":"lockPackage","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Lock package returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockOperationPackage"}}}},"400":{"description":"Invalid sensor","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockOperationPackage"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockOperationPackage"}}}}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/codes/slots":{"get":{"tags":["Smart Locks"],"summary":"List smart lock code slots","description":"Returns the current code slots and their states for a smart lock.","operationId":"listSlots","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Slot list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SmartLockCodeSlot"}}}}},"400":{"description":"Invalid sensor","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SmartLockCodeSlot"}}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SmartLockCodeSlot"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor":{"get":{"tags":["Sensors"],"summary":"Get all sensors","description":"Retrieves a complete list of all sensors available under the authenticated user's tenant.\n\n**Reading temperature and humidity.** Measurements arrive inside the node's Bluetooth\nadvertisement and are exposed on each sensor as `adv_data.sensor_data`, an uppercase hex\nstring of exactly three bytes (six characters, byte 0 first).\n\nOn temperature nodes — `type` `temperature` (node type 4) and `wall_thermostat`\n(node type 11) — the bytes decode as:\n\n| Byte | Meaning | Decoding |\n|---|---|---|\n| 0-1 | Temperature | signed 16-bit little-endian hundredths of a degree Celsius: `celsius = int16le(byte0, byte1) / 100`. `FFFF` means no valid reading. |\n| 2 | Relative humidity | unsigned byte, whole percent (`0`-`100`) |\n\nThe backend already decodes the temperature and returns it on every sensor as\n`temperatureC` in degrees Celsius; prefer that field over decoding bytes 0-1 yourself.\nWhen a node is not a temperature node, has never advertised, or reports the `FFFF`\nno-reading marker, `temperatureC` is the non-numeric value NaN, serialized as the string\n`\"NaN\"` — treat that as \"no reading\", never as a measurement. Humidity is **not** decoded\nby the backend: read byte 2 of `adv_data.sensor_data` yourself.\n\nExample: `sensor_data` `2C082D` is 0x082C = 2092 hundredths, so 20.92 °C, at 45 %\nrelative humidity.\n\nOn relay nodes (`relay`, `high_voltage_relay`) and lock nodes (`lock_8015`, `lock_s42`,\n`lock_wireless_code_panel`) byte 0 is the open state (`00` closed, `01` open) and the\nremaining bytes are unused. Other node types report `000000`.\n\nReadings are only as fresh as the last advertisement; use `lastHeardFrom` to judge age.\nFor historical or aggregate temperature use `GET /api/temperature-control/history/{sensorId}`\nand `GET /api/temperature-control/stats`. No humidity history is stored.\n","operationId":"getAllSensors","responses":{"200":{"description":"List of sensors returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sensor"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/{id}/taken":{"get":{"tags":["Sensors"],"summary":"Check if a sensor is claimed","description":"Returns true if the sensor UUID is already claimed by any tenant.","operationId":"isSensorTaken","parameters":[{"name":"id","in":"path","description":"Sensor UUID (payload.getUUIDString())","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Claim status returned successfully","content":{"*/*":{"schema":{"type":"boolean"}}}},"400":{"description":"Missing sensor id","content":{"*/*":{"schema":{"type":"boolean"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/{id}/messages":{"get":{"tags":["Sensors"],"summary":"Get all communication messages for a sensor","description":"Retrieves a chronological log of all messages exchanged with a particular sensor.\n\nUseful for debugging, diagnostics, and analytics of node communication.\n","operationId":"getAllMessages","parameters":[{"name":"id","in":"path","description":"Sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Message list returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/message/{id}/{gwid}":{"get":{"tags":["Sensors"],"summary":"Resend a message to a gateway","description":"Re-dispatches a specific message from the backend to a given gateway.\n\nTypically used when message delivery failed or needs re-acknowledgment.\n","operationId":"resendMessage","parameters":[{"name":"id","in":"path","description":"Message ID (hex string)","required":true,"schema":{"type":"string"}},{"name":"gwid","in":"path","description":"Gateway ID that should receive the message","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Message resend triggered successfully"},"404":{"description":"Message or gateway not found"}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Sensors"],"summary":"Delete a message","description":"Deletes a previously queued or sent message from the backend.","operationId":"deleteMessage","parameters":[{"name":"id","in":"path","description":"Message ID (hex string)","required":true,"schema":{"type":"string"}},{"name":"gwid","in":"path","description":"Gateway ID (unused for deletion but logged)","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Message deleted successfully"},"404":{"description":"Message not found"}},"security":[{"bearerAuth":[]}]}},"/api/sensor/groups/by-sensor/{sensorId}":{"get":{"tags":["Sensors"],"summary":"Get sensor groups by sensor id","description":"Returns all groups that include the provided sensor id.","operationId":"getGroupsBySensorId","parameters":[{"name":"sensorId","in":"path","description":"Sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Groups returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorGroup"}}}}},"security":[{"bearerAuth":[]}]}},"/api/sensor/groups/by-gateway/{gatewayId}":{"get":{"tags":["Sensors"],"summary":"Get sensor groups by gateway id","description":"Returns all groups that include the provided gateway id.","operationId":"getGroupsByGatewayId","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Groups returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorGroup"}}}}},"security":[{"bearerAuth":[]}]}},"/api/node-updates/firmwares":{"get":{"tags":["Node Updates"],"summary":"List node firmware images","description":"Returns only firmware images in /firmware that have the complete four-file offline-signed bundle: nrf_node-<version>.bin, nrf_node-<version>.manifest.bin, nrf_node-<version>.manifest.sig, and nrf_node-<version>.signer.der.","operationId":"listFirmwares","responses":{"200":{"description":"Node firmware list returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/NodeFirmwareInfo"}}}}},"security":[{"bearerAuth":[]}]}},"/api/messages":{"get":{"tags":["Protocol messages"],"summary":"Get a page of recent protocol messages","description":"Returns up to `size` tenant-scoped incoming and outgoing messages, newest first, with stable\ntimestamp and record-ID ordering. `page` is zero-based and `size` defaults to 40 (maximum 100).\n`last` and `totalElements` describe the selected history. Use the first response's `latestTimestamp`\nas `before` on later pages to keep newer records from shifting offsets; deduplicate record IDs.\n`after` optionally excludes older messages, for example after a client-side Clear log action.\n`since` continues to affect only `newMessages`, not the returned page. `latestTimestamp` is the\nnewest retained message for the tenant. Records remain in memory for about 24 hours and disappear\non process restart. The bearer token and Tenant header select the authenticated tenant.\n","operationId":"getMessages","parameters":[{"name":"since","in":"query","description":"Unix epoch milliseconds used to calculate newMessages","required":false,"schema":{"type":"integer","format":"int64"},"example":1786528800000},{"name":"page","in":"query","description":"Zero-based page index","required":false,"schema":{"type":"integer","format":"int32","default":0},"example":0},{"name":"size","in":"query","description":"Messages per page, from 1 to 100","required":false,"schema":{"type":"integer","format":"int32","default":40},"example":40},{"name":"before","in":"query","description":"Inclusive upper timestamp bound in Unix epoch milliseconds; use the first page's latestTimestamp","required":false,"schema":{"type":"integer","format":"int64"},"example":1786528800000},{"name":"after","in":"query","description":"Inclusive lower timestamp bound in Unix epoch milliseconds, for example after clearing the visible log","required":false,"schema":{"type":"integer","format":"int64"},"example":1786528700000}],"responses":{"200":{"description":"Requested page of protocol history","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageHistoryResponse"}}}},"400":{"description":"Invalid page, size or time bounds","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageHistoryResponse"}}}}},"security":[{"bearerAuth":[]}]}},"/api/guest-portal/{tenantId}/bookings/{bookingId}/room":{"get":{"tags":["Guest Portal"],"summary":"Get room name for a booking","operationId":"getRoomFromTenantPath","parameters":[{"name":"tenantId","in":"path","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"string"}}}}}}},"/api/guest-portal/{tenantId}/bookings/{bookingId}/roomName":{"get":{"tags":["Guest Portal"],"summary":"Get room name for a booking","operationId":"getRoomNameFromTenantPath","parameters":[{"name":"tenantId","in":"path","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomNameResponse"}}}}}}},"/api/guest-portal/frames/{id}":{"get":{"tags":["Guest Portal"],"summary":"Get message frame by id","operationId":"getFrame","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"query","description":"Booking ID","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","description":"Message frame ID (hex)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}}}},"/api/guest-portal/bookings/{bookingId}/validity":{"get":{"tags":["Guest Portal"],"summary":"Get booking validity status","operationId":"getBookingValidity","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingValidityResponse"}}}}}}},"/api/guest-portal/bookings/{bookingId}/timezone":{"get":{"tags":["Guest Portal"],"summary":"Get tenant timezone for a guest booking","description":"Returns only the configured tenant timezone for an existing booking in the current tenant, for example Europe/Copenhagen. The stored value is returned without a UTC fallback. No bearer token is required. Supply tenant context using the Tenant header; the authentication filter also accepts the legacy tenantId query parameter when the header is absent. The Tenant header takes precedence. The booking ID must belong to that tenant. Available before, during and after the stay, including after checkout, independently of display settings. Read-only, low risk, no approval required and safe to retry. No body, queue entry or event is produced; verify the result from the 200 response. Missing tenant context returns 400; an unknown booking, tenant or missing/blank tenant timezone returns 404.","operationId":"getGuestPortalTimezone","parameters":[{"name":"Tenant","in":"header","description":"Tenant context; required unless supplied through the legacy tenantId query parameter","schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID from the guest portal link, belonging to the current tenant","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Configured tenant timezone","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimezoneResponse"}}}},"400":{"description":"Missing tenant context"},"404":{"description":"Booking, tenant or tenant timezone not found"}},"security":[]}},"/api/guest-portal/bookings/{bookingId}/tenantName":{"get":{"tags":["Guest Portal"],"summary":"Get tenant name for a guest booking","description":"Returns only the tenant display name for an existing booking in the current tenant. No bearer token is required. Supply tenant context using the Tenant header; the authentication filter also accepts the legacy tenantId query parameter when the header is absent. The Tenant header takes precedence. The booking ID must belong to that tenant. Available before, during and after the stay, including after checkout, independently of display settings. Read-only, low risk, no approval required and safe to retry. No body, queue entry or event is produced; verify the result from the 200 response. Missing tenant context returns 400; an unknown booking, tenant or missing/blank tenant name returns 404.","operationId":"getGuestPortalTenantName","parameters":[{"name":"Tenant","in":"header","description":"Tenant context; required unless supplied through the legacy tenantId query parameter","schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID from the guest portal link, belonging to the current tenant","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Tenant display name","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantNameResponse"}}}},"400":{"description":"Missing tenant context"},"404":{"description":"Booking, tenant or tenant name not found"}},"security":[]}},"/api/guest-portal/bookings/{bookingId}/sensors":{"get":{"tags":["Guest Portal"],"summary":"List sensors for a booking","operationId":"getSensors","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Sensor"}}}}}}}},"/api/guest-portal/bookings/{bookingId}/roomcode":{"get":{"tags":["Guest Portal"],"summary":"Get room code for a booking","operationId":"getRoomCode","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"string"}}}}}}},"/api/guest-portal/bookings/{bookingId}/room":{"get":{"tags":["Guest Portal"],"summary":"Get room name for a booking","operationId":"getRoom","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"string"}}}}}}},"/api/guest-portal/bookings/{bookingId}/roomName":{"get":{"tags":["Guest Portal"],"summary":"Get room name for a booking","operationId":"getRoomName","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"string"}}}}}}},"/api/guest-portal/bookings/{bookingId}/offers":{"get":{"tags":["Guest Portal"],"summary":"List active guest portal offers for a booking","operationId":"getGuestPortalOffers","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}},{"name":"language","in":"query","description":"Language code","required":false,"schema":{"type":"string"},"example":"en"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GuestPortalOffer"}}}}}}}},"/api/guest-portal/bookings/{bookingId}/messages":{"get":{"tags":["Guest Portal"],"summary":"Get guest portal message by booking and language","operationId":"getGuestPortalMessage","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}},{"name":"language","in":"query","description":"Language code","required":true,"schema":{"type":"string"},"example":"en"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalMessage"}}}}}}},"/api/guest-portal/bookings/{bookingId}/languages":{"get":{"tags":["Guest Portal"],"summary":"List activated guest portal languages for a booking","operationId":"getActivatedLanguagesForBooking","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GuestPortalLanguageOption"}}}}}}}},"/api/guest-portal/bookings/{bookingId}/display-settings":{"get":{"tags":["Guest Portal"],"summary":"Get guest portal display toggles for a booking","operationId":"getGuestPortalDisplaySettings","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalDisplaySettingsResponse"}}}}}}},"/api/guest-portal/bookings/{bookingId}/directions":{"get":{"tags":["Guest Portal"],"summary":"Get step-by-step directions for a booking's room","operationId":"getDirections","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}},{"name":"language","in":"query","description":"Language code","required":false,"schema":{"type":"string","default":"en"},"example":"en"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalDirectionsResponse"}}}}}}},"/api/guest-portal/bookings/{bookingId}/direction-images/{imageId}":{"get":{"tags":["Guest Portal"],"summary":"Get an image referenced by a booking's direction route","operationId":"getDirectionImage","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}},{"name":"imageId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"string","format":"byte"}}}}}}}},"/api/guest-portal/bookings/{bookingId}/ai/activated":{"get":{"tags":["Guest Portal"],"summary":"Check whether AI module is activated for a booking tenant","operationId":"isAIModuleActivated","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIModuleActivatedResponse"}}}}}}},"/api/guest-portal-admin/languages":{"get":{"tags":["Guest Portal Admin"],"summary":"List all selectable languages for guest portal","operationId":"getAvailableLanguages","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GuestPortalLanguageOption"}}}}}}}},"/api/guest-portal-admin/direction-variables":{"get":{"tags":["Guest Portal Admin"],"summary":"List supported guest direction template variables","operationId":"listDirectionVariables","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/api/guest-portal-admin/direction-images/{imageId}":{"get":{"tags":["Guest Portal Admin"],"summary":"Get guest direction image metadata","operationId":"getDirectionImage_1","parameters":[{"name":"imageId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalDirectionImage"}}}}}},"delete":{"tags":["Guest Portal Admin"],"summary":"Delete a guest direction image","operationId":"deleteDirectionImage","parameters":[{"name":"imageId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/api/gateways":{"get":{"tags":["Gateways"],"summary":"List all gateways","description":"Retrieve a list of all registered Solvotix gateways in the system.","operationId":"getAll","responses":{"200":{"description":"List of gateways returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gateway"}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{id}/sensors":{"get":{"tags":["Gateways"],"summary":"List sensors connected to a gateway","description":"Returns all sensors currently associated with the specified gateway.","operationId":"getSensors_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Sensor"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{id}/refreshqueue":{"get":{"tags":["Gateways"],"summary":"Request refreshed node list","description":"Instructs a gateway to perform a fresh scan of all connected Solvotix nodes through the gateway queue. Users with the Restricted role receive 403 Forbidden.","operationId":"refreshNodeList","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Node-list refresh requested"},"403":{"description":"Gateway message operations are unavailable to the Restricted role"}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{id}/loadqueue":{"get":{"tags":["Gateways"],"summary":"Trigger queue reload","description":"Asks a gateway to refresh its message queue state and send updates back to the backend. Users with the Restricted role receive 403 Forbidden.","operationId":"loadqueue","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Queue reload requested"},"403":{"description":"Gateway message operations are unavailable to the Restricted role"}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{id}/gwqueue":{"get":{"tags":["Gateways"],"summary":"Get gateway message queue","description":"Download the currently queued messages for a specific gateway. Users with the Restricted role receive 403 Forbidden.","operationId":"getQueue","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Gateway messages returned","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MessageFrame"}}}}},"403":{"description":"Gateway messages are unavailable to the Restricted role","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MessageFrame"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{id}/action/{action}":{"get":{"tags":["Gateways"],"summary":"Execute a remote gateway action","description":"Executes a remote command or predefined action on the specified gateway.\nThis may include rebooting, rescanning, or other maintenance operations.\n","operationId":"doAction","parameters":[{"name":"id","in":"path","description":"Gateway ID","required":true,"schema":{"type":"string"}},{"name":"action","in":"path","description":"Action name to perform (e.g., 'reboot', 'scan')","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Action executed successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"404":{"description":"Gateway not found or action failed","content":{"application/json":{"schema":{"type":"boolean"}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{gatewayId}/nrffirmware":{"get":{"tags":["Gateways"],"summary":"Get NRF firmware for gateway","description":"Returns the latest NRF (Bluetooth controller) firmware image for a given gateway.\nIf the current version matches the latest, a 404 response is returned.\n","operationId":"getNrfFirmware","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID requesting NRF firmware","required":true,"schema":{"type":"string"}},{"name":"current","in":"query","description":"Current NRF firmware version","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Firmware binary returned successfully","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"No update available or firmware not found","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{gatewayId}/nodefirmware":{"get":{"tags":["Gateways"],"summary":"Get Node firmware via gateway","description":"Retrieves the latest firmware image for a connected **Solvotix Node**.\nThis is typically relayed through a gateway during OTA updates.\n","operationId":"nodeFirmware","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID managing the node","required":true,"schema":{"type":"string"}},{"name":"current","in":"query","description":"Current node firmware version","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Node firmware binary returned successfully","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"No update available or firmware not found","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{gatewayId}/metering":{"get":{"tags":["Gateways"],"summary":"Get gateway metering data","description":"Returns a bounded, sequence-ordered page of raw metering records for a gateway.","operationId":"getMeteringData","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID","required":true,"schema":{"type":"string"}},{"name":"subAction","in":"query","description":"Optional metering subaction: 12, 13, 14, or 15","required":false,"schema":{"type":"integer","format":"int32"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":100}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageGatewayMeteringRecord"}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{gatewayId}/metering/latest":{"get":{"tags":["Gateways"],"summary":"Get latest gateway metering record","description":"Returns the metering record with the highest sequence for a gateway.","operationId":"getLatestMeteringData","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayMeteringRecord"}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{gatewayId}/firmware":{"get":{"tags":["Gateways"],"summary":"Get firmware for gateway or sensor OTA","description":"Retrieves the latest firmware binary for a Solvotix gateway or thermostat sensor OTA request.\nOnly returns data if a newer version is available than the one currently installed.\n","operationId":"getFirmware","parameters":[{"name":"gatewayId","in":"path","description":"Gateway ID requesting firmware","required":true,"schema":{"type":"string"}},{"name":"current","in":"query","description":"Currently installed firmware version","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Firmware binary returned successfully","content":{"application/octet-stream":{}}},"404":{"description":"No update available or firmware not found","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/queue/device/{deviceId}":{"get":{"tags":["Gateways"],"summary":"Get outbound queue for a device","description":"Returns all pending outbound queue messages targeted at a specific device (node) ID.","operationId":"getQueueForDevice","parameters":[{"name":"deviceId","in":"path","description":"Device (node) ID to filter the queue by","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Queue messages returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"403":{"description":"Gateway messages are unavailable to the Restricted role","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MessageFrame"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/queue/device/{deviceId}/packages":{"get":{"tags":["Gateways"],"summary":"Download transferable queue packages for a device","description":"Returns the pending outbound queue for one device as complete node-core packages in\nqueue order. The existing message IDs, timestamps, payloads, and checksums are\npreserved. Reading this endpoint does not remove or acknowledge any queue entries.\nIt also does not reserve entries or suspend normal gateway delivery, so callers must\nprevent concurrent manual and gateway delivery. Payloads can contain sensitive data.\n","operationId":"getTransferPackagesForDevice","parameters":[{"name":"deviceId","in":"path","description":"Device (node) ID to filter the tenant queue by","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Transferable queue packages returned","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/QueuedDevicePackage"}}}}},"403":{"description":"Gateway messages are unavailable to the Restricted role","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/QueuedDevicePackage"}}}}},"500":{"description":"A queued package could not be serialized","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/QueuedDevicePackage"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/getqueue":{"get":{"tags":["Gateways"],"summary":"Get outbound queue for current tenant","description":"Returns all pending outbound queue messages scoped to the tenant resolved from the JWT.","operationId":"getQueueForCurrentTenant","responses":{"200":{"description":"Queue messages returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayQueueMessage"}}}},"403":{"description":"Gateway messages are unavailable to the Restricted role","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GatewayQueueMessage"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/events/sensor/":{"get":{"tags":["Events"],"summary":"Get events for a sensor in a time period","description":"Retrieves Solvotix events for the specified sensor UUID within the given time period.\nThe range is inclusive and both `from` and `to` must be ISO-8601 timestamps.\nThe bearer token and required `Tenant` header supply tenant context. Results may include raw device-protocol actions and\napplication actions; interpret `sub_action` only in the context of `action`.\n","operationId":"getEventsForSensor","parameters":[{"name":"from","in":"query","description":"Inclusive range start in ISO-8601 format","required":true,"schema":{"type":"string"},"example":"2026-04-01T00:00:00Z"},{"name":"to","in":"query","description":"Inclusive range end in ISO-8601 format","required":true,"schema":{"type":"string"},"example":"2026-04-15T23:59:59Z"}],"responses":{"200":{"description":"Sensor events retrieved successfully","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SolvotixEvent"}}}}},"400":{"description":"Invalid sensor UUID or time range"}},"security":[{"bearerAuth":[]}]}},"/api/events/sensor/{sensorUuid}":{"get":{"tags":["Events"],"summary":"Get events for a sensor in a time period","description":"Retrieves Solvotix events for the specified sensor UUID within the given time period.\nThe range is inclusive and both `from` and `to` must be ISO-8601 timestamps.\nThe bearer token and required `Tenant` header supply tenant context. Results may include raw device-protocol actions and\napplication actions; interpret `sub_action` only in the context of `action`.\n","operationId":"getEventsForSensor_1","parameters":[{"name":"sensorUuid","in":"path","description":"Sensor UUID to filter by","required":true,"schema":{"type":"string"},"example":"A1B2C3D4E5F60708"},{"name":"from","in":"query","description":"Inclusive range start in ISO-8601 format","required":true,"schema":{"type":"string"},"example":"2026-04-01T00:00:00Z"},{"name":"to","in":"query","description":"Inclusive range end in ISO-8601 format","required":true,"schema":{"type":"string"},"example":"2026-04-15T23:59:59Z"}],"responses":{"200":{"description":"Sensor events retrieved successfully","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SolvotixEvent"}}}}},"400":{"description":"Invalid sensor UUID or time range"}},"security":[{"bearerAuth":[]}]}},"/api/events/sensor":{"get":{"tags":["Events"],"summary":"Get events for a sensor in a time period","description":"Retrieves Solvotix events for the specified sensor UUID within the given time period.\nThe range is inclusive and both `from` and `to` must be ISO-8601 timestamps.\nThe bearer token and required `Tenant` header supply tenant context. Results may include raw device-protocol actions and\napplication actions; interpret `sub_action` only in the context of `action`.\n","operationId":"getEventsForSensor_2","parameters":[{"name":"from","in":"query","description":"Inclusive range start in ISO-8601 format","required":true,"schema":{"type":"string"},"example":"2026-04-01T00:00:00Z"},{"name":"to","in":"query","description":"Inclusive range end in ISO-8601 format","required":true,"schema":{"type":"string"},"example":"2026-04-15T23:59:59Z"}],"responses":{"200":{"description":"Sensor events retrieved successfully","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SolvotixEvent"}}}}},"400":{"description":"Invalid sensor UUID or time range"}},"security":[{"bearerAuth":[]}]}},"/api/events/page":{"get":{"tags":["Events"],"summary":"Get a page of tenant events","description":"Returns events in descending creation-time and ID order. Pages are zero-based and contain 40 events by default.\nOptional inclusive `from` and `to` ISO-8601 timestamps must be supplied together. Use `sensorUuid` or `roomId`\nto filter the stream; these filters are mutually exclusive and a room filter requires a time range.\nThe bearer token and required `Tenant` header provide tenant context. Receptionist results have access codes censored.\nA later page request reflects current data, so clients should deduplicate event IDs when new events arrive.\n","operationId":"getEventsPage","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":40}},{"name":"from","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","in":"query","required":false,"schema":{"type":"string"}},{"name":"sensorUuid","in":"query","required":false,"schema":{"type":"string"}},{"name":"roomId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Page of events, including content, totalElements and last","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page"}}}},"400":{"description":"Invalid page, size, filters or time range"},"404":{"description":"Room not found"}},"security":[{"bearerAuth":[]}]}},"/api/events/byRoom/{roomId}":{"get":{"tags":["Events"],"summary":"Get room events in a time period","description":"Retrieves Solvotix events connected to a room within the given time period.\nThe result includes booking lifecycle events, events connected to matching bookings,\nlock code add/remove events for room sensors, and lock-user entry events for room sensors.\nBoth `startdate` and `enddate` must be ISO-8601 timestamps.\nThe bearer token and required `Tenant` header supply tenant context. For booking action 1034, only\n`data.state=checked_out` identifies a checkout transition; `checkedOut=true` is only a snapshot.\n","operationId":"byRoom","parameters":[{"name":"roomId","in":"path","description":"Room id to filter by","required":true,"schema":{"type":"string"},"example":"room-101"},{"name":"startdate","in":"query","description":"Inclusive range start in ISO-8601 format","required":true,"schema":{"type":"string"},"example":"2026-04-01T00:00:00Z"},{"name":"enddate","in":"query","description":"Inclusive range end in ISO-8601 format","required":true,"schema":{"type":"string"},"example":"2026-04-15T23:59:59Z"}],"responses":{"200":{"description":"Room events retrieved successfully","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SolvotixEvent"}}}}},"400":{"description":"Invalid room id or time range"},"404":{"description":"Room not found"}},"security":[{"bearerAuth":[]}]}},"/api/cleaning/rooms":{"get":{"tags":["Cleaning"],"summary":"List rooms with cleaning status","operationId":"listRooms","responses":{"200":{"description":"List of tracked rooms","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CleaningRoom"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms/{roomId}/codes/random/pick":{"get":{"tags":["Bookings"],"summary":"Pick random room code","description":"Selects an available uploaded room code or generates one using the tenant's roomCodeLength setting (4-7 digits; default 4), depending on the configured picker strategy. A newly generated code is not usable until uploaded to every room lock; locks with existing codes of another length reject it.","operationId":"pickRandomRoomCode","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Random code returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeResponse"}}}},"404":{"description":"Room not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeResponse"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms/{roomId}/codes/health":{"get":{"tags":["Bookings"],"summary":"Check room-code health","description":"Returns one row per stored room code, including confirmation status for every assigned code-capable lock and the ids of locks missing the code. This read-only check does not queue or resend commands.","operationId":"getRoomCodeHealth","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Room-code health table","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeHealthResponse"}}}},"400":{"description":"Room id is missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeHealthResponse"}}}},"404":{"description":"Room not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomCodeHealthResponse"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/categories":{"get":{"tags":["Bookings"],"summary":"List categories","operationId":"listCategories","responses":{"200":{"description":"List of categories","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Categories"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/automation":{"get":{"tags":["Automation"],"summary":"List automation scenarios","description":"Returns the built-in automation sequences available for building operations.","operationId":"listAutomationScenarios","responses":{"200":{"description":"List of automation scenarios","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AutomationCapability"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/automation/messages":{"get":{"tags":["Automated Messages"],"summary":"List automated messages","description":"Provides the default messages emitted when automation triggers fire.","operationId":"listAutomatedMessages","responses":{"200":{"description":"List of automated messages","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AutomationMessage"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/automation/messages/triggers/config":{"get":{"tags":["Automated Messages"],"summary":"List supported trigger types","description":"Returns the available trigger configuration options that can be created.","operationId":"listTriggerDefinitions","responses":{"200":{"description":"List of available triggers","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AutomationTriggerDefinition"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/automation/messages/logs/bookings/{bookingId}":{"get":{"tags":["Automated Messages"],"summary":"List sent-message logs for a booking","description":"Returns the current tenant's successful automated-message delivery logs for the booking, newest first. Each record can include recipient contact details, rendered email/SMS content, delivered channels, room code, and dispatch time. An unknown booking id or a booking without successful deliveries returns an empty list.","operationId":"listMessageLogsForBooking","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Booking message-delivery logs, newest first; empty when no matching logs exist","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AutomatedMessageLog"}}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AutomatedMessageLog"}}}}},"403":{"description":"Receptionist role required","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AutomatedMessageLog"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/automation/locks/triggers/config":{"get":{"tags":["Lock Automation"],"summary":"List supported lock automation triggers","description":"Returns the triggers a task can fire. Use the returned `key` as the `trigger` field\nwhen creating or updating a task. The list is static and identical for every tenant.\n","operationId":"listTriggerDefinitions_1","responses":{"200":{"description":"List of supported triggers","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LockAutomationTriggerDefinition"}}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LockAutomationTriggerDefinition"}}}}},"403":{"description":"Superuser access required","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LockAutomationTriggerDefinition"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/ai/threads":{"get":{"tags":["AI Chat Threads"],"summary":"List all AI chat threads","description":"Returns a summary view of all AI chat threads for the current tenant.","operationId":"listThreads","responses":{"200":{"description":"Thread summaries returned","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AIChatThreadSummary"}}}}}},"security":[{"bearerAuth":[]}]}},"/api/ai/threads/{sessionId}":{"get":{"tags":["AI Chat Threads"],"summary":"Get full AI chat thread","description":"Returns the entire chat thread, including all messages, for a summary item selected by session id.","operationId":"getThread","parameters":[{"name":"sessionId","in":"path","description":"Session id of the thread to fetch.","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Thread returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIChatThread"}}}},"404":{"description":"Thread not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIChatThread"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["AI Chat Threads"],"summary":"Delete AI chat thread","description":"Deletes a thread by session id.","operationId":"deleteThread","parameters":[{"name":"sessionId","in":"path","description":"Session id of the thread to delete.","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Thread deleted"},"404":{"description":"Thread not found"}},"security":[{"bearerAuth":[]}]}},"/api/users/{id}/account":{"delete":{"tags":["Users"],"summary":"Delete user account and all related data","description":"Removes the user's memberships across their tenants and related records, including:\n\n- Tenant membership (security)\n- Push notification registrations\n- Email notification registrations\n\nIf the user no longer belongs to any tenants after this operation,\ntheir Firebase Authentication account may also be deleted on the backend.\n\nWhen this removes the last user from a tenant, the shared tenant cleanup deletes its\ngateway registrations and releases their global ownership claims. It disconnects\nregistered gateway transports, removes runtime state and cached gateway queues, and\nreleases node connection ownership. Those gateway IDs can then be claimed in another\ntenant with POST /api/gateways/{id}/{name}. Tenants with remaining users retain their\ngateways. This does not factory-reset hardware or confirm cancellation of commands\nalready delivered to hardware.\n\nRequires bearer authentication and tenant context supplied through the Tenant header.\nThere is no request body. Confirm the user ID, all affected tenants and explicit\napproval before this destructive operation. Success returns 200 with no body; it is\nnot a hardware acknowledgement. No dedicated gateway-deletion event is emitted.\nVerify removal using an identity still authorized to inspect the affected tenant, or\nverify a subsequent approved claim and gateway details in the destination tenant.\nDo not automatically retry after a timeout: inspect remaining memberships and state\nfirst because cleanup spans multiple records and may have partially completed.\n","operationId":"deleteAccountAndRelatedData","parameters":[{"name":"id","in":"path","description":"User ID whose account should be deleted","required":true,"schema":{"type":"string"},"example":"u_001"}],"responses":{"200":{"description":"Account and related data deleted successfully"},"401":{"description":"Unauthorized"}},"security":[{"bearerAuth":[]}]}},"/api/users/pushnotification/{id}":{"delete":{"tags":["Users"],"summary":"Delete a specific push notification registration by ID","operationId":"deleteNotificationById","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"bearerAuth":[]}]}},"/api/users/emailnotification/{id}":{"delete":{"tags":["Users"],"summary":"Delete a specific email notification registration by ID","description":"Removes the email notification preference with the given identifier.","operationId":"deleteEmailNotificationById","parameters":[{"name":"id","in":"path","description":"Email registration ID","required":true,"schema":{"type":"string"},"example":"eml_001"}],"responses":{"200":{"description":"Deleted successfully"},"401":{"description":"Unauthorized"},"404":{"description":"Registration not found"}},"security":[{"bearerAuth":[]}]}},"/api/smartlocks/{id}/codes":{"delete":{"tags":["Smart Locks"],"summary":"Delete all access codes","description":"Deletes all access codes on the smart lock.","operationId":"deleteAllCodes","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Delete codes command queued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid sensor or payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]}},"/api/node-updates/sensors/{sensorId}/queue":{"delete":{"tags":["Node Updates"],"summary":"Remove all queued messages for a sensor","description":"Deletes queued messages for the tenant-owned sensor only when no OTA session or OTA package is active. Returns 409 without changing the queue during OTA; use DELETE /api/node-updates/sensors/{sensorId} to cancel the update first.","operationId":"removeAllQueuedMessages","parameters":[{"name":"sensorId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Messages removed, returns count","content":{"*/*":{"schema":{"type":"integer","format":"int32"}}}},"400":{"description":"Invalid sensor ID","content":{"*/*":{"schema":{"type":"integer","format":"int32"}}}},"409":{"description":"OTA update active; use the dedicated abort endpoint","content":{"*/*":{"schema":{"type":"integer","format":"int32"}}}}},"security":[{"bearerAuth":[]}]}},"/api/lock-users/{id}/wallet-certificates/{walletCertificateId}":{"delete":{"tags":["Lock Users"],"summary":"Remove wallet certificate from lock user","description":"Removes the assignment and queues removal from a lock only when no room or other lock user still grants the same wallet credential on that lock.","operationId":"removeWalletCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"walletCertificateId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/lockusers/{id}/wallet-certificates/{walletCertificateId}":{"delete":{"tags":["Lock Users"],"summary":"Remove wallet certificate from lock user","description":"Removes the assignment and queues removal from a lock only when no room or other lock user still grants the same wallet credential on that lock.","operationId":"removeWalletCertificate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"walletCertificateId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/lockusers/{id}/mifare-credentials/{mifareUid}":{"delete":{"tags":["Lock Users"],"summary":"Remove MIFARE credential from lock user","description":"Removes one MIFARE card credential from a lock user and queues its removal from every assigned lock sensor that no other lock user still grants it on. Removing a UID the lock user does not have returns the unchanged lock user.","operationId":"removeMifareCredential","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"mifareUid","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Credential removed from the lock user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"400":{"description":"MIFARE UID is not 4, 7, or 10 bytes of hex","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"404":{"description":"Lock user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/lock-users/{id}/mifare-credentials/{mifareUid}":{"delete":{"tags":["Lock Users"],"summary":"Remove MIFARE credential from lock user","description":"Removes one MIFARE card credential from a lock user and queues its removal from every assigned lock sensor that no other lock user still grants it on. Removing a UID the lock user does not have returns the unchanged lock user.","operationId":"removeMifareCredential_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"mifareUid","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Credential removed from the lock user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"400":{"description":"MIFARE UID is not 4, 7, or 10 bytes of hex","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"404":{"description":"Lock user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}}},"security":[{"bearerAuth":[]}]}},"/api/gateways/{id}/message/{message_id}":{"delete":{"tags":["Gateways"],"summary":"Delete a message on a gateway","description":"Removes a specific message from the gateway's queue.","operationId":"deleteMessageOnGateway","parameters":[{"name":"id","in":"path","description":"Gateway ID","required":true,"schema":{"type":"string"}},{"name":"message_id","in":"path","description":"Message ID to delete","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Message deleted successfully"},"403":{"description":"Gateway messages are unavailable to the Restricted role"},"404":{"description":"Message not found or already deleted"}},"security":[{"bearerAuth":[]}]}},"/api/gateways/queue/message/{messageId}":{"delete":{"tags":["Gateways"],"summary":"Delete a message from the outbound queue","description":"Removes a specific message from the outbound message queue by its message ID.","operationId":"removeMessageFromQueue","parameters":[{"name":"messageId","in":"path","description":"Message ID to remove from the queue","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Message removed from queue successfully"},"403":{"description":"Gateway messages are unavailable to the Restricted role"},"404":{"description":"Message not found in queue"}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms/{roomId}/wallet-certificates/{walletCertificateId}":{"delete":{"tags":["Bookings"],"summary":"Remove wallet certificate from room","description":"Removes the assignment and queues removal from each room lock unless another room or lock user still grants it there.","operationId":"removeRoomWalletCertificate","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}},{"name":"walletCertificateId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomWalletCertificates"}}}}},"security":[{"bearerAuth":[]}]}},"/api/bookings/rooms/{roomId}/codes/{code}":{"delete":{"tags":["Bookings"],"summary":"Remove room code","description":"Removes a specific 4-7 digit code from the room.","operationId":"removeRoomCode","parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}},{"name":"code","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated room","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}}},"404":{"description":"Room not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rooms"}}}}},"security":[{"bearerAuth":[]}]}},"/api/api-users/{id}":{"delete":{"tags":["API users"],"summary":"Revoke an API user","description":"Disables the API user and invalidates its token immediately.","operationId":"revoke","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"API user revoked"},"403":{"description":"Caller is an API user or lacks tenant access"},"404":{"description":"API user not found in the selected tenant"}},"security":[{"bearerAuth":[]}]}}},"components":{"schemas":{"WalletCertificate":{"required":["platform"],"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"tenantId":{"type":"string","description":"Tenant id populated from authenticated request context.","readOnly":true},"platform":{"type":"string","description":"Phone wallet that receives the card.","enum":["APPLE","ANDROID"]},"name":{"type":"string","description":"Short card name shown to administrators and on the wallet card.","example":"Main entrance"},"companyName":{"type":"string","description":"Clean company display text. Blank values become 'Your company'.","example":"Your company"},"description":{"type":"string","description":"Optional card description.","example":"Mobile access card"},"logoUrl":{"type":"string","description":"Card artwork URL. Blank values use the Solvotix logo published on solvotix.net."},"lockUserId":{"type":"string","description":"Most recently assigned lock-user id summary. Use the dedicated assignment endpoints.","readOnly":true},"credentialId":{"type":"string","description":"Immutable hexadecimal credential embedded in the phone pass and uploaded to assigned locks.","readOnly":true},"validFrom":{"type":"string","description":"Optional activation time.","format":"date-time"},"validUntil":{"type":"string","description":"Optional expiration time.","format":"date-time"},"active":{"type":"boolean","description":"Whether an install package may be generated.","default":true}},"description":"Tenant-owned wallet card definition. Signing keys and service-account credentials are never stored in or returned with this object."},"User":{"type":"object","properties":{"uid":{"type":"string","description":"The id of the user","example":"UUID"},"email":{"type":"string","description":"The email of the user","example":"ola@norman.no"},"name":{"type":"string","description":"The full name of the user","example":"Ola Norman"},"language":{"type":"string","description":"Preferred UI language for the user","example":"en"},"last_sign_in":{"type":"string","description":"The date when last signed in","format":"date-time"},"roles":{"type":"array","description":"Roles assigned to the user. If empty, the user has unrestricted access. Restricted has broad access but masks access-code values and cannot access gateway messages.","example":["User"],"items":{"type":"string","description":"Roles assigned to the user. If empty, the user has unrestricted access. Restricted has broad access but masks access-code values and cannot access gateway messages.","example":"[\"User\"]"}}},"description":"Information about the user"},"DevicePushRegistration":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"fcmToken":{"type":"string"},"platform":{"type":"string"},"deviceModel":{"type":"string"},"tenantId":{"type":"string"},"deviceId":{"type":"string"},"enabled":{"type":"boolean"}}},"UserPushNotificationSettings":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"tenantId":{"type":"string","description":"Tenant id owning these settings","example":"tenant123"},"gatewayOnline":{"type":"boolean","description":"Notify when a gateway comes online","example":true},"gatewayOffline":{"type":"boolean","description":"Notify when a gateway goes offline","example":true},"sensorOffline":{"type":"boolean","description":"Notify when a sensor goes offline","example":true},"sensorOnline":{"type":"boolean","description":"Notify when a sensor comes online","example":true},"alarmActive":{"type":"boolean","description":"Notify when an alarm is active","example":true},"alarmCleared":{"type":"boolean","description":"Notify when an alarm is cleared","example":true},"sensorBatteryLow":{"type":"boolean","description":"Notify when sensor battery is low","example":true},"smartLockCodeUploadDelayed":{"type":"boolean","description":"Notify when a smart lock code upload is delayed","example":true},"smartLockValidCodeRejected":{"type":"boolean","description":"Notify when a smart lock rejects a code marked valid and uploaded in the backend","example":true}},"description":"Per-user push notification settings for the current tenant"},"EmailNotificationRegistration":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"enabled":{"type":"boolean","description":"Whether event emails are enabled for this user","example":true}},"description":"Per-user email notification preference (tenant is implied from headers)"},"GatewayWifiCredentials":{"required":["password","ssid"],"type":"object","properties":{"ssid":{"type":"string","description":"Network SSID, 1 to 32 UTF-8 bytes. Identifies the saved network; saving the same exact SSID updates its password. Preserved exactly, including whitespace and case.","example":"Building Wi-Fi"},"password":{"type":"string","description":"Network password, at most 64 UTF-8 bytes. Required; use an empty string for an open network. Preserved exactly. Returned unmasked for reuse; never log it.","format":"password","example":"<WIFI_PASSWORD>"}},"description":"An entry in the current tenant's saved gateway Wi-Fi network list. The exact SSID identifies the entry. Both fields are returned to authorized clients; treat them as secrets."},"Tenant":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"name":{"type":"string","description":"Name of the tenant","example":"Acme Corporation"},"description":{"type":"string","description":"A description for the tenant","example":"The story of your life"},"contactEmail":{"type":"string","description":"Primary contact email address for the tenant","example":"contact@example.com"},"diagnosticEmail":{"type":"string","description":"Email address for diagnostic correspondence","example":"diagnostics@example.com"},"contactPhoneNumber":{"type":"string","description":"Primary contact phone number for the tenant","example":"+4512345678"},"contactName":{"type":"string","description":"Primary contact name for the tenant","example":"Jane Smith"},"timezone":{"type":"string","description":"IANA time zone identifier used for scheduling and display","example":"Europe/Oslo"},"profile":{"type":"string","description":"The kind of property this tenant operates. Descriptive metadata only; it does not change device behaviour. Missing or unrecognized values resolve to undefined.","example":"hotel","default":"undefined","enum":["hotel","office","private","storage","camping","agriculture","undefined"]},"soundLevel":{"type":"string","description":"Default node sound level used in boot configuration: off, low, medium, or high. Missing or unrecognized values resolve to low.","example":"low","default":"low","enum":["off","low","medium","high"]},"nfcEnabled":{"type":"boolean","description":"Tenant-wide node 10 NFC-reader setting used in boot configuration. Missing values default to false.","example":false,"default":false},"accessMode":{"type":"string","description":"Tenant-wide access mode for keypad locks, controllers and wireless code panels (node types 7, 8, 9 and 10). standard closes again on its own after a valid code, openUntilClosed keeps the lock open until * is pressed or a close is sent, and forcedClosed refuses every local code or card so the lock can only be opened from the backend or the physical system menu. Missing values default to standard.","example":"standard","default":"standard","enum":["standard","openUntilClosed","forcedClosed"]},"roomCodeLength":{"maximum":7,"minimum":4,"type":"integer","description":"Length of automatically generated and selected room access codes. Existing codes of another length remain stored but are not selected automatically. Missing values default to 4. All codes on a lock must have the same length.","format":"int32","example":5,"default":4},"systemCode":{"pattern":"^\\d{6}$","type":"string","description":"Six-digit code used to enter the physical system menu. Missing or invalid values disable the menu. Treat this value as a secret.","example":"123456"},"smtpHost":{"type":"string","description":"Optional SMTP host to override the default mail server","example":"smtp.example.com"},"smtpPort":{"type":"integer","description":"SMTP port for the tenant-specific mail server","format":"int32","example":587},"smtpUsername":{"type":"string","description":"Username for authenticating with the tenant-specific mail server"},"smtpPassword":{"type":"string","description":"Password for authenticating with the tenant-specific mail server"},"smtpFrom":{"type":"string","description":"From address to use when sending emails for this tenant","example":"Alerts <alerts@example.com>"},"smtpFromName":{"type":"string","description":"Display name to use in the From header when sending emails","example":"Solvotix Support"},"defaultFromName":{"type":"string","description":"Default formatted From identity used with the global mail server when tenant-specific SMTP is not configured","example":"Your hotel <no.reply@solvotix.org>","default":"Your hotel <no.reply@solvotix.org>"},"imapHost":{"type":"string","description":"IMAP host used for reading inbound emails","example":"imap.zoho.eu"},"imapPort":{"type":"integer","description":"IMAP port used for inbound mailbox access","format":"int32","example":993},"imapSsl":{"type":"boolean","description":"Whether SSL is required for IMAP connections","example":true},"instructionsForParsing":{"type":"string","description":"Custom instructions used when parsing lead-related input/content"},"instructionsForMailGeneration":{"type":"string","description":"Custom instructions used when generating lead-related emails"},"smsSender":{"type":"string","description":"Sender name/number to use when sending SMS for this tenant","example":"MyBuilding"}},"description":"Tenant object identifying which tenant to delete"},"SmartLockConfigurationRequest":{"type":"object","properties":{"soundEnabled":{"type":"boolean","description":"Enable or disable keypad sounds.","example":true},"soundLevel":{"type":"string","description":"Keypad sound level: off, low, medium, or high.","example":"low"},"systemCode":{"type":"string","description":"Six-digit code used to enter the physical lock system menu.","example":"123456"},"setTime":{"type":"boolean","description":"Whether boot configuration should update the node clock.","example":true,"default":true},"lightEnabled":{"type":"boolean","description":"Enable or disable keypad light.","example":false},"openSeconds":{"type":"integer","description":"Seconds to keep the lock open.","format":"int32","example":6}},"description":"Smart lock configuration payload."},"Data":{"type":"object","properties":{"raw":{"type":"array","items":{"type":"string","format":"byte"}}}},"GatewaysDelivered":{"type":"object","properties":{"date":{"type":"string","format":"date-time"},"gw_id":{"type":"string"},"isAdding":{"type":"boolean"}}},"MessageFrame":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"msg":{"$ref":"#/components/schemas/server_message_t"},"uuid":{"type":"string"},"bookingId":{"type":"string"},"gatewayDelivery":{"type":"array","items":{"$ref":"#/components/schemas/GatewaysDelivered"}},"deliveredTime":{"type":"string","format":"date-time"}}},"NodeCoreMessage":{"type":"object","properties":{"id":{"type":"array","items":{"type":"string","format":"byte"}},"timestamp":{"type":"integer","format":"int32"},"action":{"type":"string","format":"byte"},"sub_action":{"type":"string","format":"byte"},"data":{"$ref":"#/components/schemas/Data"},"checksum":{"type":"string","format":"byte"},"idHex":{"type":"string"}}},"adv_payload_t":{"type":"object","properties":{"status":{"type":"string","format":"byte"},"node_type":{"type":"string","format":"byte"},"checksum":{"type":"string","format":"byte"},"battery_level":{"type":"string","format":"byte"},"timestamp":{"type":"integer","format":"int32"},"uuidstring":{"type":"string"},"uuid":{"type":"string"},"node_settings":{"type":"string","description":"Three node setting bytes as uppercase hex, byte 0 first. Byte 0 is a bit field: mask 0x0F is the advertising power profile 0-9, mask 0x10 is set while a lock node is held open with nothing scheduled to close it (a backend open, a forced open, a system-menu unlock, or an open-until-closed latch; clear for a timed open, and on the wireless code panel it describes the paired relays), and mask 0x80 marks a node using the encrypted communication layout. Mask the byte rather than comparing it whole. The open bit requires node firmware v115 or later and reads 0 on older firmware.","example":"040000"},"fw_version":{"type":"string"},"sensor_data":{"pattern":"^[0-9A-F]{6}$","type":"string","description":"The three measurement bytes from the advertisement, uppercase hex, most significant nibble first, three bytes = six characters (byte 0 is the first character pair).\n\nTemperature nodes (type `temperature`, node type 4, and `wall_thermostat`, node type 11):\nbytes 0-1 are the temperature in hundredths of a degree Celsius as a signed 16-bit little-endian value (byte 0 low, byte 1 high), so `celsius = int16le(byte0, byte1) / 100`. `FFFF` means the node has no valid reading. Byte 2 is the relative humidity in whole percent as an unsigned byte (`0`-`100`). The backend decodes the temperature for you and exposes it as the sensor field `temperatureC`; humidity is not decoded by the backend, so read byte 2 of this field to obtain it.\n\nRelay nodes (`relay`, `high_voltage_relay`) and lock nodes (`lock_8015`, `lock_s42`, `lock_wireless_code_panel`): byte 0 is the open state (`00` closed, `01` open); bytes 1-2 are unused. Other node types do not populate this field and report `000000`.\n\nExample `2C082D`: temperature bytes `2C 08` are 0x082C = 2092, so 20.92 °C, and humidity byte `2D` is 45 % relative humidity.","example":"2C082D"}},"description":"Latest Bluetooth advertisement received from a node, as stored on the sensor."},"server_message_t":{"type":"object","properties":{"magic":{"type":"string","format":"byte"},"uuid":{"type":"string"},"gw_id":{"type":"string"},"message":{"$ref":"#/components/schemas/NodeCoreMessage"},"adv_payload":{"$ref":"#/components/schemas/adv_payload_t"},"status":{"type":"string","format":"byte"},"delivered":{"type":"boolean"},"nrf_version":{"type":"array","items":{"type":"string","format":"byte"}},"rssi":{"type":"string","format":"byte"},"fireAndForget":{"type":"boolean"},"alarm":{"type":"boolean"},"gwMessage":{"type":"boolean"},"nrfVersion":{"type":"integer","format":"int32"}}},"SmartLockGlobalConfiguration":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"tenantId":{"type":"string"},"masterCode":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"}}},"SmartLockAccessModeConfiguration":{"type":"object","properties":{"accessMode":{"type":"string","description":"Access mode applied to all smart locks:\nstandard — a valid code opens the lock and it closes again on its own;\nopenUntilClosed — a valid code keeps the lock open until * is pressed on the keypad,\na close command is sent, or the physical system menu locks it;\nforcedClosed — local codes and cards are refused and reported as invalid, so the lock\ncan only be opened from the backend or from the physical system menu.\n","example":"standard","default":"standard","enum":["standard","openUntilClosed","forcedClosed"]},"allowedModes":{"type":"array","description":"All access modes this backend accepts.","items":{"type":"string","description":"All access modes this backend accepts."}}},"description":"Tenant-wide smart lock access mode. The mode applies to every keypad lock, lock controller\nand wireless code panel in the tenant (node types 7, 8, 9 and 10) and is delivered to the\ndevices in their boot configuration.\n"},"Sensor":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"name":{"type":"string"},"type":{"type":"string"},"location":{"type":"string"},"fw_version":{"type":"integer","format":"int32"},"unit":{"type":"string"},"active":{"type":"boolean"},"lastHeardFrom":{"type":"string","format":"date-time"},"isNew":{"type":"boolean"},"adv_data":{"$ref":"#/components/schemas/adv_payload_t"},"isUpdating":{"type":"boolean"},"progress":{"type":"integer","format":"int32"},"updateByGwId":{"type":"string"},"numberOfMessagesToDeliver":{"type":"integer","format":"int32"},"rssi":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}},"batteryHistory":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}},"seenByGateways":{"uniqueItems":true,"type":"array","items":{"type":"string"}},"configuration":{"type":"object","additionalProperties":{"type":"object"}},"pairedDevices":{"type":"array","items":{"$ref":"#/components/schemas/SensorPairedDevice"}},"pushNotificationDelayMinutes":{"type":"integer","format":"int32"},"state":{"type":"string"},"gatewayWithBestRSSI":{"type":"string"},"relay":{"type":"boolean"},"uuid":{"type":"string"},"activeAlarm":{"type":"boolean"},"wirelessCodePanel":{"type":"boolean"},"codeLock":{"type":"boolean"},"temperatureSensor":{"type":"boolean"},"temperatureC":{"type":"number","description":"Temperature in degrees Celsius, decoded from bytes 0-1 of adv_data.sensor_data (signed 16-bit little-endian hundredths of a degree). Only nodes of type `temperature` and `wall_thermostat` report a temperature; every other node type, a node that has not advertised yet, and the no-reading marker `FFFF` all yield the non-numeric value NaN, which is serialized as the string \"NaN\". Humidity is not decoded here: read it from byte 2 of adv_data.sensor_data as an unsigned percentage.","format":"double","readOnly":true,"example":20.92},"advertisementData":{"$ref":"#/components/schemas/adv_payload_t"}}},"SensorPairedDevice":{"type":"object","properties":{"sensorId":{"type":"string"},"addedToDevice":{"type":"boolean"}}},"SensorPairingRequest":{"type":"object","properties":{"sensorIds":{"type":"array","items":{"type":"string"}}}},"TemperatureControlConfiguration":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"min_temp":{"type":"integer","description":"Minimum temperature threshold","format":"int32","example":18},"max_temp":{"type":"integer","description":"Maximum temperature threshold","format":"int32","example":26},"pms_room_in_use_temp":{"type":"integer","description":"Target temperature when PMS reports room in use","format":"int32","example":21},"pms_room_not_in_use_temp":{"type":"integer","description":"Target temperature when PMS reports room not in use","format":"int32","example":17}},"description":"Per-tenant temperature control configuration settings."},"SensorGroup":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"name":{"type":"string","description":"Optional display name for the group","example":"Floor 2 East Wing"},"sensorIds":{"type":"array","description":"Sensor ids included in the group","items":{"type":"string","description":"Sensor ids included in the group"}},"gatewayIds":{"type":"array","description":"Gateway ids included in the group","items":{"type":"string","description":"Gateway ids included in the group"}}},"description":"Group of sensors and gateways within a tenant."},"RelayConfigurationRequest":{"type":"object","properties":{"openMilliseconds":{"type":"integer","description":"Relay open duration in milliseconds (1-255).","format":"int32","example":150}},"description":"Relay configuration payload."},"ModuleToggle":{"type":"object","properties":{"key":{"type":"string","description":"Unique key for the module","example":"automation.temperature"},"title":{"type":"string","description":"Display title for the module","example":"Temperature Control"},"description":{"type":"string","description":"Short description of what the module provides"},"highlights":{"type":"array","description":"Bulleted highlights shown in the UI","items":{"type":"string","description":"Bulleted highlights shown in the UI"}},"enabled":{"type":"boolean","description":"Whether the module is active for this tenant"},"category":{"type":"string","description":"Category of the module","example":"automation"}},"description":"Tenant-scoped module toggle with presentation metadata."},"TenantModuleSettingsResponse":{"type":"object","properties":{"automation":{"type":"array","description":"Automation modules with activation state","items":{"$ref":"#/components/schemas/ModuleToggle"}},"integrations":{"type":"array","description":"Integration modules with activation state","items":{"$ref":"#/components/schemas/ModuleToggle"}}},"description":"Module activation state merged with presentation metadata."},"TenantModuleSettingsRequest":{"type":"object","properties":{"automation":{"type":"object","additionalProperties":{"type":"boolean","description":"Automation module states keyed by module id"},"description":"Automation module states keyed by module id"},"integrations":{"type":"object","additionalProperties":{"type":"boolean","description":"Integration module states keyed by module id"},"description":"Integration module states keyed by module id"}},"description":"Payload for updating tenant module activation toggles."},"LockUser":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"sensorIds":{"type":"array","description":"Smart lock sensor IDs assigned to this user.","items":{"type":"string","description":"Smart lock sensor IDs assigned to this user."}},"codes":{"type":"array","description":"Access codes assigned to this user.","items":{"type":"string","description":"Access codes assigned to this user."}},"mifareCredentials":{"type":"array","description":"MIFARE card credentials assigned to this user. Each credential may carry an optional PIN.","items":{"$ref":"#/components/schemas/LockUserMifareCredential"}},"walletCertificateIds":{"type":"array","description":"Wallet certificate IDs assigned to this user and queued to its compatible locks.","items":{"type":"string","description":"Wallet certificate IDs assigned to this user and queued to its compatible locks."}},"codesNotUploadedToAllSensors":{"type":"integer","description":"Number of assigned lock/code pairs that have not been uploaded to the lock yet.","format":"int32"},"sensorIdsWithCodesNotUploaded":{"type":"array","description":"Distinct assigned code-capable sensor IDs with at least one numeric access code not confirmed uploaded. Uses the same checks as codesNotUploadedToAllSensors; empty when none are pending. Computed by the server, including for Restricted users.","readOnly":true,"items":{"type":"string","description":"Distinct assigned code-capable sensor IDs with at least one numeric access code not confirmed uploaded. Uses the same checks as codesNotUploadedToAllSensors; empty when none are pending. Computed by the server, including for Restricted users.","readOnly":true}},"mifareCredentialsNotUploadedToAllSensors":{"type":"integer","description":"Number of assigned lock/MIFARE-credential pairs that have not been uploaded to the lock yet.","format":"int32"},"name":{"type":"string","description":"Display name for the lock user."},"description":{"type":"string","description":"Optional description for the lock user."},"email":{"type":"string","description":"Email address for the lock user."},"phone":{"type":"string","description":"Phone number for the lock user."}},"description":"User access profile for smart lock codes."},"LockUserMifareCredential":{"type":"object","properties":{"mifareUid":{"type":"string","description":"MIFARE UID as hex without separators. Must be 4, 7, or 10 bytes, i.e. 8, 14, or 20 hex characters. Stored and returned uppercase.","example":"DEADBEEF"},"code":{"type":"string","description":"Optional 4-7 digit PIN that must be entered after the card is presented. When omitted the card alone opens the lock. When set it must have the same length as the other access codes already on the lock.","nullable":true,"example":"1234"}},"description":"MIFARE card credential assigned to a lock user, with an optional PIN that must be entered together with the card. Requires a smart lock with the extended credential store (lock_8015, lock_s42)."},"GuestPortalMessage":{"type":"object","properties":{"roomId":{"type":"string","description":"Room identifier this message applies to.","example":"room-101"},"roomIds":{"type":"array","description":"Room identifiers this message applies to. Use this to share one message across rooms.","example":["room-101","room-102"],"items":{"type":"string","description":"Room identifiers this message applies to. Use this to share one message across rooms.","example":"[\"room-101\",\"room-102\"]"}},"message":{"type":"string","description":"Message shown to the guest in the portal.","example":"Welcome! Your room is ready."},"language":{"type":"string","description":"Language code for the message. Omit it to make this the room default for all languages.","example":"en"}},"description":"Custom guest portal message entry."},"GuestPortalOffer":{"type":"object","properties":{"id":{"type":"string","description":"Stable offer identifier"},"title":{"type":"string","description":"Offer title","example":"Late checkout"},"description":{"type":"string","description":"Offer body text","example":"Ask reception about late checkout availability."},"imageUrl":{"type":"string","description":"Optional image URL for the offer"},"encodedImage":{"type":"string","description":"Optional encoded image data for the offer, for example a Base64 data URL"},"linkUrl":{"type":"string","description":"Optional target URL opened when the guest selects the offer"},"buttonText":{"type":"string","description":"Optional button text","example":"View offer"},"language":{"type":"string","description":"Language code for this offer. Leave blank to show in all languages.","example":"en"},"active":{"type":"boolean","description":"Whether the offer should be shown","example":true},"priority":{"type":"integer","description":"Lower values are shown first","format":"int32","example":10},"validFrom":{"type":"string","description":"Offer is visible from this time, if set","format":"date-time"},"validTo":{"type":"string","description":"Offer is visible until this time, if set","format":"date-time"}},"description":"Offer or ad displayed in the guest portal."},"GuestPortalSettings":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"showRoom":{"type":"boolean","description":"Whether room name should be visible in guest portal.","example":true},"showCode":{"type":"boolean","description":"Whether access code should be visible in guest portal.","example":true},"limitToValidTime":{"type":"boolean","description":"Whether guest portal actions are limited to booking valid time window.","example":true},"showOpenButton":{"type":"boolean","description":"Whether the open button should be visible in guest portal.","example":true},"showCheckoutButton":{"type":"boolean","description":"Whether the checkout button should be visible in guest portal.","example":true},"activatedLanguages":{"type":"array","description":"Language codes activated for display in the guest portal.","example":["en","no"],"items":{"type":"string","description":"Language codes activated for display in the guest portal.","example":"[\"en\",\"no\"]"}},"messages":{"type":"array","description":"Custom guest portal messages configured for rooms and languages.","items":{"$ref":"#/components/schemas/GuestPortalMessage"}},"offers":{"type":"array","description":"Offers and ads displayed in the guest portal.","items":{"$ref":"#/components/schemas/GuestPortalOffer"}},"generalMessageTemplate":{"type":"string","description":"General message template prepended to all room messages. Use {roominstructions} placeholder to inject the room-specific message.","example":"Follow the room instructions: {roominstructions}."}},"description":"Tenant-level guest portal settings."},"GuestPortalActivatedLanguagesRequest":{"type":"object","properties":{"activatedLanguages":{"type":"array","description":"Language codes activated for the guest portal.","example":["en","no"],"items":{"type":"string","description":"Language codes activated for the guest portal.","example":"[\"en\",\"no\"]"}}},"description":"Request payload for guest portal activated languages."},"GuestPortalLanguageOption":{"type":"object","properties":{"code":{"type":"string","description":"ISO 639-1 language code.","example":"en"},"name":{"type":"string","description":"Language name in English.","example":"English"},"nativeName":{"type":"string","description":"Language native display name.","example":"English"}},"description":"Selectable language option for the guest portal."},"GuestPortalDirectionRoute":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"tenantId":{"type":"string"},"name":{"type":"string"},"active":{"type":"boolean"},"position":{"type":"integer","format":"int32"},"translations":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/GuestPortalDirectionTranslation"}},"roomIds":{"type":"array","items":{"type":"string"}},"steps":{"type":"array","items":{"$ref":"#/components/schemas/GuestPortalDirectionStep"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"description":"Reusable, room-assigned guest direction route."},"GuestPortalDirectionStep":{"type":"object","properties":{"id":{"type":"string"},"position":{"type":"integer","format":"int32"},"imageId":{"type":"string"},"translations":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/GuestPortalDirectionTranslation"}}}},"GuestPortalDirectionTranslation":{"type":"object","properties":{"titleTemplate":{"type":"string"},"descriptionTemplate":{"type":"string"},"imageAltTextTemplate":{"type":"string"}}},"Gateway":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"gatewayId":{"type":"string"},"isOnline":{"type":"boolean"},"pushNotificationDelayMinutes":{"type":"integer","description":"Minutes without a gateway heartbeat before it is marked offline and an offline warning is sent. Defaults to 5 minutes; must be positive.","format":"int32","example":20},"mainPowerInUse":{"type":"boolean"},"lastHeardFrom":{"type":"string","format":"date-time"},"description":{"type":"string"},"name":{"type":"string"},"tenantId":{"type":"string"},"nodes":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/adv_payload_t"}},"node_last_heard_from":{"type":"object","additionalProperties":{"type":"string","format":"date-time"}},"isNodeOnline":{"type":"object","additionalProperties":{"type":"boolean"}},"version":{"type":"string"},"nrf_version":{"type":"integer","format":"int32"},"firmwareUpdate":{"type":"boolean"},"downloadedNodeVersion":{"type":"integer","format":"int32"},"downloadedNrfVersion":{"type":"integer","format":"int32"},"isUpdating":{"type":"boolean"},"progress":{"type":"integer","format":"int32"},"gw_state":{"type":"integer","format":"int32"},"connectedToNodeId":{"type":"string"},"pendingNodeId":{"type":"string"},"gatewayStateVersion":{"type":"integer","format":"int32"},"gatewayStateSeq":{"type":"integer","format":"int64"},"gatewayStateAgeMs":{"type":"integer","format":"int64"},"scanRunning":{"type":"boolean"},"connectTaskExists":{"type":"boolean"},"activeConnectionExists":{"type":"boolean"},"activeUuidValid":{"type":"boolean"},"smartplug_state":{"type":"string"},"connected":{"type":"boolean"},"ready":{"type":"boolean"},"tenantOwner":{"type":"string"}}},"Event":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"message":{"$ref":"#/components/schemas/server_message_t"},"reason":{"type":"string"},"bookingId":{"type":"string"}}},"EmailBrandingSettings":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"tenantId":{"type":"string","description":"Tenant id owning these settings","example":"tenant123"},"name":{"type":"string","description":"Display name shown in the email header/footer","example":"Solvotix"},"logoUrl":{"type":"string","description":"Optional absolute HTTP(S) logo URL. Used when logoBase64 is empty; remote images may be blocked by the recipient.","example":"https://cdn.example.com/logo.png"},"template":{"type":"string","description":"Email layout. Omitted/null on update preserves the saved layout; blank resets to classic. Existing settings without a layout render as classic.","example":"classic","enum":["classic","modern","minimal","elegant"]},"primaryColor":{"pattern":"^#[0-9A-Fa-f]{6}$","type":"string","description":"Optional Modern-template primary color as a six-digit hex code. Controls the header and accent. Omitted/null on update preserves the saved value; blank resets to the default #08788C. Ignored by other templates.","example":"#08788C","default":"#08788C"},"secondaryColor":{"pattern":"^#[0-9A-Fa-f]{6}$","type":"string","description":"Optional Modern-template secondary color as a six-digit hex code. Controls the outer background and main text. Omitted/null on update preserves the saved value; blank resets to the default #001838. Ignored by other templates.","example":"#001838","default":"#001838"},"logoBase64":{"maxLength":699075,"type":"string","description":"Uploaded PNG or JPEG as data:image/png;base64,... or data:image/jpeg;base64,...; maximum 524288 decoded bytes and 2048 pixels per side. Takes precedence over logoUrl and is sent as a CID inline MIME image. Omitted/null on update preserves the upload; empty string removes it. Returned on GET/PUT."},"website":{"type":"string","description":"Website URL shown in the email header","example":"https://www.solvotix.org"},"addressLine":{"type":"string","description":"Address line shown in the email footer","example":"Nordre Sønset 18, 3158 Andebu, Norway"},"supportEmail":{"type":"string","description":"Support email shown in the email footer","example":"support@solvotix.org"},"supportPhone":{"type":"string","description":"Support phone shown in the email footer","example":"+47 22 55 88 99"}},"description":"Per-tenant email branding configuration."},"CleaningProgramSettings":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"updateCheckInTimeOnClean":{"type":"boolean","description":"Whether cleaning updates should also update check-in time.","example":false},"showBookings":{"type":"boolean","description":"Whether bookings should be visible in the cleaning room view.","example":true},"showBookingCode":{"type":"boolean","description":"Whether booking room codes should be visible in the cleaning room view.","example":true},"earliestStartHour":{"type":"integer","description":"Earliest hour a guest should be granted access, using 24-hour format.","format":"int32","example":8},"earliestStartMinute":{"type":"integer","description":"Earliest minute a guest should be granted access.","format":"int32","example":0}},"description":"Tenant-level configuration for the cleaning program."},"CleaningRoom":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"roomId":{"type":"string","description":"Room identifier","example":"room-101"},"roomName":{"type":"string","description":"Display name of the room","example":"Room 101"},"categoryId":{"type":"string","description":"Category identifier for the room","example":"deluxe"},"sensorIds":{"type":"array","description":"Sensors assigned to the room","items":{"type":"string","description":"Sensors assigned to the room"}},"cleaned":{"type":"boolean","description":"Whether the room is currently marked as cleaned"},"cleanedAt":{"type":"string","description":"When the room was marked as cleaned","format":"date-time"},"cleanedByUserId":{"type":"string","description":"User id of the staff member who last updated the cleaning status"},"occupied":{"type":"boolean","description":"Whether the room is currently occupied"},"bookings":{"type":"array","description":"Bookings relevant to the tenant's current date for this room","items":{"$ref":"#/components/schemas/CleaningRoomBooking"}}},"description":"Cleaning status snapshot for a room."},"CleaningRoomBooking":{"type":"object","properties":{"bookingId":{"type":"string","description":"Booking identifier","example":"booking-123"},"guestName":{"type":"string","description":"Booking guest name","example":"Jane Doe"},"email":{"type":"string","description":"Booking email","example":"jane@example.com"},"phone":{"type":"string","description":"Booking phone","example":"+4512345678"},"roomCode":{"type":"string","description":"Room code assigned to the booking","example":"1234"},"start":{"type":"string","description":"Booking start time","format":"date-time"},"end":{"type":"string","description":"Booking end time","format":"date-time"},"arrivingToday":{"type":"boolean","description":"Whether the booking arrives on the tenant's current date"},"leavingToday":{"type":"boolean","description":"Whether the booking leaves on the tenant's current date"},"stayingToday":{"type":"boolean","description":"Whether the booking stays on the tenant's current date"},"checkedIn":{"type":"boolean","description":"Whether the booking is currently checked in"},"checkedOut":{"type":"boolean","description":"Whether the booking is currently checked out"}},"description":"Booking details relevant to the cleaning room view."},"CleaningStatusUpdateRequest":{"type":"object","properties":{"cleaned":{"type":"boolean","description":"Set to true when a room has been cleaned, false to reset it.","example":true}},"description":"Request payload for updating a room cleaning status."},"RoomCodeEntry":{"type":"object","properties":{"code":{"type":"string"},"taken":{"type":"boolean"},"takenAt":{"type":"string","format":"date-time"},"takenByBookingId":{"type":"string"}}},"Rooms":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"roomId":{"type":"string"},"roomName":{"type":"string"},"categoryId":{"type":"string"},"sensorIds":{"type":"array","items":{"type":"string"}},"codes":{"type":"array","items":{"$ref":"#/components/schemas/RoomCodeEntry"}},"excludedRandomCodeDigits":{"type":"array","description":"Decimal digits excluded from automatic generation and automatic selection of room codes. Empty means no room-specific exclusions.","example":["5","8"],"items":{"type":"string","description":"Decimal digits excluded from automatic generation and automatic selection of room codes. Empty means no room-specific exclusions.","example":"[\"5\",\"8\"]"}}}},"RoomCodeGenerationSettingsResponse":{"type":"object","properties":{"roomsUpdated":{"type":"integer","description":"Number of rooms whose setting was processed.","format":"int32"},"codesRemoved":{"type":"integer","description":"Unused stored codes removed because they contained an excluded digit.","format":"int32"},"replacementCodesCreated":{"type":"integer","description":"Compliant unused replacement codes created.","format":"int32"},"activeCodesRetained":{"type":"integer","description":"Conflicting codes retained because they are currently assigned to bookings.","format":"int32"},"rooms":{"type":"array","description":"Updated rooms. Device commands are asynchronous; this response does not confirm physical lock delivery.","items":{"$ref":"#/components/schemas/Rooms"}}},"description":"Result of updating automatic room-code digit exclusions."},"RoomCodeGenerationSettingsRequest":{"type":"object","properties":{"roomIds":{"type":"array","description":"Room ids to update. Tenant context is supplied by authentication.","example":["room-a","room-b"],"items":{"type":"string","description":"Room ids to update. Tenant context is supplied by authentication.","example":"[\"room-a\",\"room-b\"]"}},"excludedDigits":{"type":"array","description":"Distinct decimal digits that automatic room-code generation must not use. At least two digits must remain available.","example":["5","8"],"items":{"type":"string","description":"Distinct decimal digits that automatic room-code generation must not use. At least two digits must remain available.","example":"[\"5\",\"8\"]"}}},"description":"Bulk room selection and digits excluded from automatically generated room codes."},"Categories":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"categoryId":{"type":"string"},"name":{"type":"string"}}},"Booking":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"email":{"type":"string"},"phone":{"type":"string"},"guestName":{"type":"string"},"language":{"type":"string"},"bookingId":{"type":"string"},"start":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"arrived":{"type":"boolean"},"arrivedAt":{"type":"string","description":"Time the booking first used its assigned room code, recorded from the lock event","format":"date-time","readOnly":true},"checkedIn":{"type":"boolean"},"checkedOut":{"type":"boolean"},"externalId":{"type":"string"},"roomId":{"type":"string"},"source":{"type":"string"},"totalAmount":{"type":"number","description":"Total gross amount for the booking in the source system's currency"},"amountPaid":{"type":"number","description":"Gross amount paid for the booking in the source system's currency"},"codeSent":{"type":"string","format":"date-time"},"externalReferenceIds":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}},"messageSent":{"type":"boolean","description":"Whether at least one automated-message channel has been delivered successfully for this booking","readOnly":true,"example":true},"automatedMessageBlockReason":{"type":"integer","description":"Current automated-message dispatch block reason: 0=ALL_OK, 1=ROOM_NOT_CLEAN, 2=NOT_PAID, 3=NO_RECIPIENT_CHANNEL, 4=DISPATCH_FAILED","format":"int32","readOnly":true,"example":0},"roomCode":{"type":"string"}}},"AutomationRoomMessage":{"type":"object","properties":{"roomIds":{"type":"array","description":"Room identifiers this template applies to.","example":["room-101","room-102"],"items":{"type":"string","description":"Room identifiers this template applies to.","example":"[\"room-101\",\"room-102\"]"}},"language":{"type":"string","description":"Language code. Omit it to make this the default for all languages.","example":"da"},"message":{"type":"string","description":"General template used by both channels when channel-specific text is absent."},"smsMessage":{"type":"string","description":"Optional SMS-specific template."},"emailMessage":{"type":"string","description":"Optional email-specific template."}},"description":"Reusable room-specific automated-message template. Templates support {departureDate} for the booking end date in the tenant timezone and regional numeric format (Europe/Oslo: 18.09.2026; America/New_York: 09/18/2026), and {departureDateText} for a lowercase English date such as 18 sep 2026. Both work in the subject, general, email and SMS bodies, including inserted room instructions. Missing booking dates produce an empty value. Missing or invalid tenant timezones use UTC; timezones without a supported country convention use yyyy-MM-dd. The booking language selects room templates but does not change date formatting. {arrivalDate} and {arrivalDateText} format the booking start with the same numeric and text rules. Use {guestportalbutton=\"Open guest portal\", color=\"#1068F0\", textColor=\"#F8F8F8\"} as a standalone body placeholder (not inside an HTML tag or link). Only email bodies render a clickable button; SMS and subjects leave this token unchanged. Use {guestportal} there. {guestportalbutton} defaults to label Open guest portal. Omitted or invalid colors default to Solvotix action blue #1068F0; color accepts #RGB or #RRGGBB. Text defaults to #F8F8F8, with #001838 on light custom backgrounds. Optional textColor overrides the text using #RGB or #RRGGBB; omitted or invalid textColor retains automatic text color. When both are supplied, color precedes textColor. Labels are HTML-escaped. Requires a booking ID and tenant context; otherwise the placeholder remains unchanged. Supported in email bodies, including general templates used as email fallback, inserted room instructions and manual email overrides. Existing {guestportal} remains a plain URL. "},"AutomationTrigger":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"tenantId":{"type":"string","description":"Tenant id for the trigger scope","example":"tenant123"},"triggerKey":{"type":"string","description":"Trigger key. when_start_time_has_passed is eligible from booking start plus offsetMinutes until booking end while not checked out, including reservations imported after arrival time. Rule edits do not reset eligibility or resend delivered channels; payment, cleanliness and recipient requirements still apply.","example":"on_day_of_arrival"},"recipient":{"type":"string","description":"Recipient of the automated message (email address or phone number)","example":"manager@example.com"},"sendToBookingGuest":{"type":"boolean","description":"Whether to deliver to the booking guest (uses guest contact details)"},"sendSms":{"type":"boolean","description":"Whether to send an SMS for this trigger"},"sendEmail":{"type":"boolean","description":"Whether to send an email for this trigger"},"sendOnlyWhenRoomClean":{"type":"boolean","description":"When true, booking-guest delivery is deferred until the assigned room is marked clean"},"onlySendIfPaidOrCheckedIn":{"type":"boolean","description":"When true, booking-guest delivery is deferred until the booking is paid in full (within a tolerance of 2) or the guest is checked in"},"message":{"type":"string","description":"Message that will be emitted (legacy/fallback)","example":"Welcome message sent on arrival day"},"smsMessage":{"type":"string","description":"SMS message content","example":"Welcome! Your room is ready."},"emailMessage":{"type":"string","description":"Email message content","example":"Welcome to the building. Your room is ready."},"roomMessages":{"type":"array","description":"Reusable room-specific templates with optional shared rooms and language overrides.","items":{"$ref":"#/components/schemas/AutomationRoomMessage"}},"title":{"type":"string","description":"Title/subject for the email","example":"Welcome to the building"},"timeOfDay":{"type":"string","description":"Time of day in HH:mm (used for day-based triggers)","example":"08:00"},"offsetMinutes":{"type":"integer","description":"Minutes relative to booking start for when_start_time_has_passed. Negative values send earlier, positive values later. Defaults to 0 for existing triggers; ignored for other trigger types. Delivery still requires an eligible recipient and stops at booking end or checkout.","format":"int32","example":-60,"default":0},"modifiedDate":{"type":"string","description":"Date the trigger was last modified","format":"date-time","example":"2023-01-02T10:15:00Z"}},"description":"Persisted automation trigger for automated messages. Use {guestportalbutton=\"Open guest portal\", color=\"#1068F0\", textColor=\"#F8F8F8\"} as a standalone body placeholder (not inside an HTML tag or link). Only email bodies render a clickable button; SMS and subjects leave this token unchanged. Use {guestportal} there. {guestportalbutton} defaults to label Open guest portal. Omitted or invalid colors default to Solvotix action blue #1068F0; color accepts #RGB or #RRGGBB. Text defaults to #F8F8F8, with #001838 on light custom backgrounds. Optional textColor overrides the text using #RGB or #RRGGBB; omitted or invalid textColor retains automatic text color. When both are supplied, color precedes textColor. Labels are HTML-escaped. Requires a booking ID and tenant context; otherwise the placeholder remains unchanged. Supported in email bodies, including general templates used as email fallback, inserted room instructions and manual email overrides. Existing {guestportal} remains a plain URL. "},"AutomationTriggerRequest":{"required":["recipient","triggerKey"],"type":"object","properties":{"triggerKey":{"type":"string","description":"Trigger key. when_start_time_has_passed sends from booking start plus offsetMinutes, or on import/update if that time has already passed and the stay has not ended or been checked out. Editing the rule does not exclude active bookings or resend already delivered channels. Configured recipient, payment and cleanliness requirements still apply.","example":"on_day_of_arrival"},"recipient":{"type":"string","description":"Recipient of the automated message (email address or phone number)","example":"manager@example.com"},"sendToBookingGuest":{"type":"boolean","description":"Whether to deliver to the booking guest (derives contact from booking)"},"sendSms":{"type":"boolean","description":"Send SMS when trigger fires"},"sendEmail":{"type":"boolean","description":"Send email when trigger fires"},"sendOnlyWhenRoomClean":{"type":"boolean","description":"Defer booking-guest delivery until the assigned room is marked clean"},"onlySendIfPaidOrCheckedIn":{"type":"boolean","description":"Defer booking-guest delivery until the booking is paid in full (within a tolerance of 2) or the guest is checked in"},"title":{"type":"string","description":"Subject/title for email notifications","example":"Welcome to the building"},"smsMessage":{"type":"string","description":"SMS body to send on trigger","example":"Welcome! Your room is ready."},"emailMessage":{"type":"string","description":"Email body to send on trigger","example":"Welcome to the building. Your room is ready."},"message":{"type":"string","description":"Fallback/general message (used if channel-specific message missing)","example":"Welcome message sent on arrival day"},"roomMessages":{"type":"array","description":"Reusable room-specific templates with shared rooms and language overrides.","items":{"$ref":"#/components/schemas/AutomationRoomMessage"}},"timeOfDay":{"type":"string","description":"Time of day in HH:mm (required for day-based triggers)","example":"08:00"},"offsetMinutes":{"type":"integer","description":"Minutes relative to booking start for when_start_time_has_passed: -60 means one hour earlier, 60 means one hour later. Omitted or null defaults to 0 on create and update. Ignored for other trigger types. Booking end, checkout, recipient, payment and cleanliness requirements still apply.","format":"int32","example":-60,"default":0}},"description":"Request for creating an automation trigger for messages. Templates support {departureDate} for the booking end date in the tenant timezone and regional numeric format (Europe/Oslo: 18.09.2026; America/New_York: 09/18/2026), and {departureDateText} for a lowercase English date such as 18 sep 2026. Both work in the subject, general, email and SMS bodies, including inserted room instructions. Missing booking dates produce an empty value. Missing or invalid tenant timezones use UTC; timezones without a supported country convention use yyyy-MM-dd. The booking language selects room templates but does not change date formatting. {arrivalDate} and {arrivalDateText} format the booking start with the same numeric and text rules. Use {guestportalbutton=\"Open guest portal\", color=\"#1068F0\", textColor=\"#F8F8F8\"} as a standalone body placeholder (not inside an HTML tag or link). Only email bodies render a clickable button; SMS and subjects leave this token unchanged. Use {guestportal} there. {guestportalbutton} defaults to label Open guest portal. Omitted or invalid colors default to Solvotix action blue #1068F0; color accepts #RGB or #RRGGBB. Text defaults to #F8F8F8, with #001838 on light custom backgrounds. Optional textColor overrides the text using #RGB or #RRGGBB; omitted or invalid textColor retains automatic text color. When both are supplied, color precedes textColor. Labels are HTML-escaped. Requires a booking ID and tenant context; otherwise the placeholder remains unchanged. Supported in email bodies, including general templates used as email fallback, inserted room instructions and manual email overrides. Existing {guestportal} remains a plain URL. "},"LockAutomationSchedule":{"required":["frequency","startDate","timeOfDay"],"type":"object","properties":{"frequency":{"type":"string","description":"How often the task repeats. ONCE fires a single time on startDate.","example":"DAILY","enum":["ONCE","DAILY","WEEKLY","MONTHLY"]},"interval":{"type":"integer","description":"Repeat every N days/weeks/months. Ignored for ONCE. Defaults to 1.","format":"int32","example":1,"default":1},"byWeekdays":{"type":"array","description":"Weekdays the task runs on. Required for WEEKLY. Accepts MON/MONDAY style names.","example":["MON","TUE","WED","THU","FRI"],"items":{"type":"string","description":"Weekdays the task runs on. Required for WEEKLY. Accepts MON/MONDAY style names.","example":"[\"MON\",\"TUE\",\"WED\",\"THU\",\"FRI\"]"}},"byMonthDays":{"type":"array","description":"Days of month (1-31) the task runs on. Required for MONTHLY. Days that do not exist\nin a given month are skipped for that month.\n","example":[1,15],"items":{"type":"integer","description":"Days of month (1-31) the task runs on. Required for MONTHLY. Days that do not exist\nin a given month are skipped for that month.\n","format":"int32"}},"timeOfDay":{"type":"string","description":"Local time of day the trigger fires, in 24-hour HH:mm format.","example":"08:00"},"startDate":{"type":"string","description":"First date the schedule is active, as YYYY-MM-DD. Recurrence intervals are counted from this date.","example":"2026-09-01"},"endDate":{"type":"string","description":"Last date the schedule is active, as YYYY-MM-DD. Omit for an open-ended schedule.","example":"2026-12-31"},"timeZone":{"type":"string","description":"IANA time zone the schedule is evaluated in. Defaults to the tenant time zone.","example":"Europe/Oslo"}},"description":"Recurring schedule for a lock automation task, modelled on calendar recurrence.\nAn occurrence happens on every date matched by the recurrence rule, at `timeOfDay`,\ninterpreted in `timeZone` (or the tenant time zone when omitted).\n"},"LockAutomationTask":{"required":["name","trigger"],"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"tenantId":{"type":"string","description":"Tenant id owning the task","example":"tenant123"},"name":{"type":"string","description":"Display name for the task","example":"Unlock main entrance"},"comment":{"type":"string","description":"Free-text comment describing why the task exists","example":"Opens the front door for the morning shift"},"trigger":{"type":"string","description":"Trigger fired on every device when an occurrence is due","example":"open","enum":["open","close","pulse_open"]},"deviceIds":{"type":"array","description":"Sensor ids of the locks, lock controllers and relays the trigger is sent to","items":{"type":"string","description":"Sensor ids of the locks, lock controllers and relays the trigger is sent to"}},"schedule":{"$ref":"#/components/schemas/LockAutomationSchedule"},"enabled":{"type":"boolean","description":"Whether the task is currently active. Disabled tasks are never executed.","example":true},"modifiedDate":{"type":"string","description":"Date the task was last modified","format":"date-time","example":"2026-08-19T10:15:00Z"},"lastFiredOccurrence":{"type":"string","description":"Local date-time of the most recently executed occurrence. Used to guarantee an occurrence fires at most once.","example":"2026-08-19T08:00"},"lastFiredAt":{"type":"string","description":"Server time of the most recent scheduled or manual dispatch attempt. It does not confirm device action.","format":"date-time","example":"2026-08-19T06:00:03Z"}},"description":"Scheduled lock automation task that fires a trigger on a set of devices on a recurring schedule."},"LockAutomationTaskRequest":{"required":["deviceIds","name","schedule","trigger"],"type":"object","properties":{"name":{"type":"string","description":"Display name for the task","example":"Unlock main entrance"},"comment":{"type":"string","description":"Free-text comment describing why the task exists","example":"Opens the front door for the morning shift"},"trigger":{"type":"string","description":"Trigger fired on every device when an occurrence is due","example":"open","enum":["open","close","pulse_open"]},"deviceIds":{"type":"array","description":"Sensor ids of the locks, lock controllers and relays the trigger is sent to. At least one is required.","example":["A1B2C3D4E5F60708"],"items":{"type":"string","description":"Sensor ids of the locks, lock controllers and relays the trigger is sent to. At least one is required.","example":"[\"A1B2C3D4E5F60708\"]"}},"schedule":{"$ref":"#/components/schemas/LockAutomationSchedule"},"enabled":{"type":"boolean","description":"Whether the task is active. Defaults to true when omitted.","example":true}},"description":"Request for creating or updating a scheduled lock automation task."},"ApiUserRolesRequest":{"type":"object","properties":{"roles":{"type":"array","description":"Canonical API-user roles. Empty means unrestricted access.","example":["Restricted"],"items":{"type":"string","description":"Canonical API-user roles. Empty means unrestricted access.","example":"[\"Restricted\"]","enum":["Janitor","Cleaning","Receptionist","Restricted","User"]},"enum":["Janitor","Cleaning","Receptionist","Restricted","User"]}},"description":"Roles assigned to a tenant-bound API user"},"ApiUser":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"name":{"type":"string"},"tenantId":{"type":"string"},"enabled":{"type":"boolean"},"expiresAt":{"type":"string","format":"date-time"},"roles":{"type":"array","description":"Roles assigned to this API user. Empty means unrestricted access. Restricted masks access codes and cannot access gateway messages.","example":["Restricted"],"items":{"type":"string","description":"Roles assigned to this API user. Empty means unrestricted access. Restricted masks access codes and cannot access gateway messages.","example":"[\"Restricted\"]","enum":["Janitor","Cleaning","Receptionist","Restricted","User"]},"enum":["Janitor","Cleaning","Receptionist","Restricted","User"]},"token":{"type":"string","description":"Plaintext token, returned only when created or rotated"}},"description":"A tenant-bound machine identity"},"AISettings":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"tenantId":{"type":"string","description":"Tenant id for the settings scope.","example":"tenant123"},"instructions":{"type":"string","description":"Instructions used to guide AI behavior.","example":"Be concise, polite, and focus on check-in support."}},"description":"Tenant-level AI settings."},"AndroidWalletPackage":{"type":"object","properties":{"saveUrl":{"type":"string","description":"Signed Save to Google Wallet URL to open on the phone."},"expiresAt":{"type":"string","description":"UTC instant after which the signed URL must be regenerated."}},"description":"Android Save to Google Wallet package response."},"ThermostatWifiRequest":{"type":"object","properties":{"ssid":{"type":"string"},"password":{"type":"string"},"gw_id":{"type":"string"}}},"ThermostatTargetTemperatureRequest":{"type":"object","properties":{"temperature":{"type":"number","format":"double"},"gw_id":{"type":"string"}}},"SmartLockCodesRequest":{"type":"object","properties":{"codes":{"type":"array","description":"Access codes (4-7 digits each).","example":["1234","98765"],"items":{"type":"string","description":"Access codes (4-7 digits each).","example":"[\"1234\",\"98765\"]"}}},"description":"Request payload containing smart lock access codes."},"SensorActionRequest":{"type":"object","properties":{"action":{"type":"string"},"subAction":{"type":"string"},"value":{"type":"string"},"userId":{"type":"string"},"gw_id":{"type":"string"}},"description":"Action request payload"},"RelayOperationPackage":{"type":"object","properties":{"sensorId":{"type":"string","description":"Relay sensor ID.","example":"849FAD3A11223344"},"operation":{"type":"string","description":"Requested relay operation.","example":"pulse"},"action":{"type":"integer","description":"Protocol action value.","format":"int32","example":29},"subAction":{"type":"integer","description":"Protocol sub-action value.","format":"int32","example":5},"consumptionPulseCount":{"type":"integer","description":"Calculated CF pulse count for a consumption operation; null for other operations.","format":"int64","nullable":true,"example":3263784},"messageId":{"type":"string","description":"Generated message ID in hexadecimal.","example":"A1B2C3D4E5F6"},"encrypted":{"type":"boolean","description":"True when the package is sealed with the relay's active secure session. Action and sub-action above are the plaintext values. An encrypted package uses a send sequence and must be delivered promptly: the node rejects it once 32 newer frames have reached it."},"packageBase64":{"type":"string","description":"Complete 212-byte node-core package encoded as Base64. Decode and transmit unchanged."},"packageHex":{"type":"string","description":"The same complete package encoded as 424 uppercase hexadecimal characters."}},"description":"A newly generated 212-byte node-core package for direct relay delivery. Generation does not queue, deliver, acknowledge, or operate the relay."},"RelayOperationPackageRequest":{"required":["operation"],"type":"object","properties":{"operation":{"type":"string","description":"Operation to encode: open, close, pulse, or consumption.","example":"pulse"},"value":{"type":"integer","description":"Pulse duration. Required only for pulse operations.","format":"int64","example":5},"unit":{"type":"string","description":"Duration unit for pulse operations: milliseconds, seconds, or minutes.","example":"seconds"},"kwh":{"minimum":0,"type":"number","description":"Energy allowance in kilowatt-hours. Required only for consumption operations. The backend converts this to the relay's CF pulse count.","example":1.5}},"description":"Relay operation to encode for direct delivery to a relay."},"NodeUpdateRequest":{"type":"object","properties":{"version":{"type":"string","description":"Target firmware version. For smart locks, crossing the firmware-111 storage boundary changes the on-device code-store format: upgrades retain slots 1-987 but require slots 988-1980 to be pushed again; downgrades require every code to be pushed again and return device communication to the legacy plaintext protocol.","example":"111"}}},"NodeUpdateStartResponse":{"type":"object","properties":{"sensorId":{"type":"string"},"gatewayId":{"type":"string"},"version":{"type":"string"},"filename":{"type":"string"},"status":{"type":"string"}}},"WalletCertificateAssignmentRequest":{"required":["walletCertificateId"],"type":"object","properties":{"walletCertificateId":{"type":"string","description":"Tenant-owned wallet certificate id."}},"description":"Assigns one existing wallet certificate to a lock user or room."},"GuestCheckoutResponse":{"type":"object","properties":{"bookingId":{"type":"string"},"roomId":{"type":"string"},"checkedIn":{"type":"boolean"},"checkedOut":{"type":"boolean"},"checkInTime":{"type":"string"},"checkoutTime":{"type":"string"},"roomMarkedDirty":{"type":"boolean"}}},"AIChatMessage":{"type":"object","properties":{"role":{"type":"string","description":"Sender role for the message.","example":"assistant"},"text":{"type":"string","description":"Message text.","example":"Hello, how can I help with your booking?"},"createdDate":{"type":"string","description":"When the message was created.","format":"date-time"}},"description":"Single message within an AI chat thread."},"AIChatThread":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"tenantId":{"type":"string","description":"Tenant id for thread scope.","example":"tenant123"},"bookingId":{"type":"string","description":"Booking id this thread belongs to.","example":"booking-123"},"sessionId":{"type":"string","description":"Public session id used by the chat API.","example":"a6b8d9c0-3a0d-4d1d-8f69-8d6310de5d8a"},"messages":{"type":"array","description":"Messages currently stored in the thread.","items":{"$ref":"#/components/schemas/AIChatMessage"}}},"description":"Persisted AI chat thread linked to a booking."},"GuestPortalDirectionImage":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"tenantId":{"type":"string"},"fileName":{"type":"string"},"contentType":{"type":"string"},"encodedImage":{"type":"string","writeOnly":true},"sizeBytes":{"type":"integer","format":"int64"},"width":{"type":"integer","format":"int32"},"height":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"RoomWalletCertificates":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"roomId":{"type":"string","description":"Room business identifier."},"walletCertificateIds":{"type":"array","description":"Wallet certificate IDs assigned to every compatible lock in the room.","items":{"type":"string","description":"Wallet certificate IDs assigned to every compatible lock in the room."}}},"description":"Wallet certificate assignments for one room."},"RoomCodeResponse":{"type":"object","properties":{"code":{"type":"string"},"room":{"$ref":"#/components/schemas/Rooms"}}},"RoomCodeResyncResponse":{"type":"object","properties":{"roomsScanned":{"type":"integer","description":"Total rooms inspected.","format":"int32"},"roomsWithCodes":{"type":"integer","description":"Rooms that had at least one valid stored room code.","format":"int32"},"codeCapableLocks":{"type":"integer","description":"Code-capable room lock links inspected.","format":"int32"},"codesConsidered":{"type":"integer","description":"Valid room-code values considered across rooms.","format":"int32"},"invalidCodesSkipped":{"type":"integer","description":"Invalid stored room-code values skipped.","format":"int32"},"addCommandsSubmitted":{"type":"integer","description":"Smart-lock add-code command requests submitted after duplicate/pending checks.","format":"int32"}},"description":"Summary of a room code resync run."},"BookingMessageRequest":{"type":"object","properties":{"email":{"type":"string","description":"Override email for the guest","example":"guest@example.com"},"phone":{"type":"string","description":"Override phone number for the guest","example":"+4712345678"},"triggerId":{"type":"string","description":"Optional automation trigger id to use. Defaults to the first booking-guest trigger for the tenant."},"smsMessage":{"type":"string","description":"Optional override for the SMS body used when sending."},"emailMessage":{"type":"string","description":"Optional override for the email body used when sending."},"subject":{"type":"string","description":"Optional override for the email subject."},"language":{"type":"string","description":"Optional language used to select localized room-message templates.","example":"en"}},"description":"Request payload for sending an automated message to a booking guest. Templates support {departureDate} for the booking end date in the tenant timezone and regional numeric format (Europe/Oslo: 18.09.2026; America/New_York: 09/18/2026), and {departureDateText} for a lowercase English date such as 18 sep 2026. Both work in the subject, general, email and SMS bodies, including inserted room instructions. Missing booking dates produce an empty value. Missing or invalid tenant timezones use UTC; timezones without a supported country convention use yyyy-MM-dd. The booking language selects room templates but does not change date formatting. {arrivalDate} and {arrivalDateText} format the booking start with the same numeric and text rules. Use {guestportalbutton=\"Open guest portal\", color=\"#1068F0\", textColor=\"#F8F8F8\"} as a standalone body placeholder (not inside an HTML tag or link). Only email bodies render a clickable button; SMS and subjects leave this token unchanged. Use {guestportal} there. {guestportalbutton} defaults to label Open guest portal. Omitted or invalid colors default to Solvotix action blue #1068F0; color accepts #RGB or #RRGGBB. Text defaults to #F8F8F8, with #001838 on light custom backgrounds. Optional textColor overrides the text using #RGB or #RRGGBB; omitted or invalid textColor retains automatic text color. When both are supplied, color precedes textColor. Labels are HTML-escaped. Requires a booking ID and tenant context; otherwise the placeholder remains unchanged. Supported in email bodies, including general templates used as email fallback, inserted room instructions and manual email overrides. Existing {guestportal} remains a plain URL. "},"BookingMessagePreview":{"type":"object","properties":{"subject":{"type":"string"},"emailBody":{"type":"string"},"smsBody":{"type":"string"},"channels":{"type":"array","items":{"type":"string"}}},"description":"Preview of the automated message that will be sent for a booking."},"BookingMessagePreviewResponse":{"type":"object","properties":{"status":{"type":"string","description":"Status of the preview request","example":"SENT","enum":["SENT","BOOKING_NOT_FOUND","NO_TRIGGER_CONFIGURED","NO_RECIPIENT"]},"message":{"type":"string","description":"Human friendly message for the current status"},"preview":{"$ref":"#/components/schemas/BookingMessagePreview"}},"description":"Preview response including status and rendered content."},"BookingRoomCodeRequest":{"type":"object","properties":{"code":{"type":"string","description":"Room access code (4-7 digits).","example":"1234"}},"description":"Request payload for assigning a specific room code to a booking."},"DeviceResult":{"type":"object","properties":{"deviceId":{"type":"string","description":"Target sensor ID"},"queued":{"type":"boolean","description":"Whether a command was queued; this does not prove physical completion"},"status":{"type":"string","description":"Queue result: queued, not_found, invalid_device, invalid_request, or failed"}},"description":"One queue outcome for each configured device"},"LockAutomationRunResult":{"type":"object","properties":{"taskId":{"type":"string","description":"ID of the task that was run"},"runId":{"type":"string","description":"Unique ID of this manual run, useful for correlating events"},"trigger":{"type":"string","description":"Trigger configured on the task","enum":["open","close","pulse_open"]},"requestedAt":{"type":"string","description":"Server time when the manual run was requested","format":"date-time"},"devices":{"type":"array","description":"One queue outcome for each configured device","items":{"$ref":"#/components/schemas/DeviceResult"}}},"description":"Outcome of one manual lock automation run. Queued commands have not necessarily acted on the physical devices."},"AIChatReplyResponse":{"type":"object","properties":{"bookingId":{"type":"string","description":"Booking id connected to the thread.","example":"booking-123"},"userMessage":{"$ref":"#/components/schemas/AIChatMessage"},"assistantMessage":{"$ref":"#/components/schemas/AIChatMessage"},"messages":{"type":"array","description":"Full ordered message history after the send.","items":{"$ref":"#/components/schemas/AIChatMessage"}},"session_id":{"type":"string","description":"Session id of the AI chat thread.","example":"a6b8d9c0-3a0d-4d1d-8f69-8d6310de5d8a"}},"description":"AI chat reply result after sending a message."},"AIChatReplyRequest":{"type":"object","properties":{"text":{"type":"string","description":"Text to send to the AI thread.","example":"Can you draft a check-in email for this guest?"},"session_id":{"type":"string","description":"Session id of the AI chat thread.","example":"a6b8d9c0-3a0d-4d1d-8f69-8d6310de5d8a"}},"description":"Request payload for sending a message to an AI chat thread."},"TemperatureStatsResponse":{"type":"object","properties":{"averageTempC":{"type":"number","description":"Average temperature in Celsius, when available","format":"double","example":21.4},"lowestTempC":{"type":"number","description":"Lowest temperature in Celsius, when available","format":"double","example":18.1},"highestTempC":{"type":"number","description":"Highest temperature in Celsius, when available","format":"double","example":24.7},"averageMessage":{"type":"string","description":"Display message for average temperature","example":"Coming soon"},"lowestMessage":{"type":"string","description":"Display message for lowest temperature","example":"Coming soon"},"highestMessage":{"type":"string","description":"Display message for highest temperature","example":"Coming soon"}},"description":"Latest aggregate temperature statistics for the current tenant."},"TemperatureSensorHistory":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"sensorId":{"type":"string","description":"Sensor id for this reading","example":"A1B2C3D4E5F60708"},"temperatureC":{"type":"number","description":"Temperature in Celsius","format":"double","example":21.7}},"description":"Periodic temperature reading for a specific sensor."},"SmartLockOperationPackage":{"type":"object","properties":{"sensorId":{"type":"string","description":"Smart lock sensor ID.","example":"849FAD3A11223344"},"operation":{"type":"string","description":"Requested lock operation.","example":"pulse-open"},"action":{"type":"integer","description":"Protocol action value.","format":"int32","example":33},"subAction":{"type":"integer","description":"Protocol sub-action value.","format":"int32","example":1},"messageId":{"type":"string","description":"Generated message ID in hexadecimal.","example":"A1B2C3D4E5F6"},"encrypted":{"type":"boolean","description":"True when the package is sealed with the lock's active secure session. Action and sub-action above are the plaintext values. An encrypted package uses a send sequence and must be delivered promptly: the node rejects it once 32 newer frames have reached it."},"packageBase64":{"type":"string","description":"Complete 212-byte node-core package encoded as Base64. Decode and transmit unchanged."},"packageHex":{"type":"string","description":"The same complete package encoded as 424 uppercase hexadecimal characters."}},"description":"A newly generated 212-byte node-core package for direct smart-lock delivery. Generation does not queue, deliver, acknowledge, or execute the command."},"SmartLockCodeSlot":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"deviceId":{"type":"string"},"slotNumber":{"type":"integer","format":"int32"},"code":{"type":"string"},"mifareUid":{"type":"string"},"walletCredentialId":{"type":"string"},"walletPlatform":{"type":"string","description":"Phone wallet that receives the card.","enum":["APPLE","ANDROID"]},"state":{"type":"integer","format":"int32"},"addedAt":{"type":"string","format":"date-time"},"uploadedToLockAt":{"type":"string","format":"date-time"},"warnedAboutNotUploaded":{"type":"boolean"}}},"NodeFirmwareInfo":{"type":"object","properties":{"filename":{"type":"string"},"version":{"type":"string"},"size":{"type":"integer","format":"int64"},"latest":{"type":"boolean"}}},"MessageHistoryResponse":{"type":"object","properties":{"messages":{"type":"array","items":{"$ref":"#/components/schemas/MessageRecord"}},"newMessages":{"type":"integer","format":"int32"},"latestTimestamp":{"type":"integer","format":"int64"},"totalElements":{"type":"integer","format":"int32"},"last":{"type":"boolean"},"page":{"type":"integer","format":"int32"}}},"MessageRecord":{"type":"object","properties":{"recordId":{"type":"string"},"timestamp":{"type":"integer","format":"int64"},"direction":{"type":"string"},"gatewayId":{"type":"string"},"nodeId":{"type":"string"},"messageId":{"type":"string"},"status":{"type":"integer","format":"int32"},"delivered":{"type":"boolean"},"message":{"$ref":"#/components/schemas/server_message_t"}}},"RoomNameResponse":{"type":"object","properties":{"roomName":{"type":"string"}}},"BookingValidityResponse":{"type":"object","properties":{"status":{"type":"integer","format":"int32"},"date":{"type":"string"},"checkInTime":{"type":"string"},"checkoutTime":{"type":"string"},"checkedIn":{"type":"boolean"},"checkedOut":{"type":"boolean"}}},"TimezoneResponse":{"type":"object","properties":{"timezone":{"type":"string","description":"Stored tenant timezone identifier","example":"Europe/Copenhagen"}},"description":"Guest-facing configured tenant timezone"},"TenantNameResponse":{"type":"object","properties":{"tenantName":{"type":"string","description":"Stored tenant name","example":"Example Hotel"}},"description":"Guest-facing tenant display name; contains no tenant configuration or credentials"},"GuestPortalDisplaySettingsResponse":{"type":"object","properties":{"showRoom":{"type":"boolean"},"showCode":{"type":"boolean"},"showOpenButton":{"type":"boolean"},"showCheckoutButton":{"type":"boolean"},"limitToValidTime":{"type":"boolean"}}},"GuestPortalDirectionsResponse":{"type":"object","properties":{"bookingId":{"type":"string"},"roomId":{"type":"string"},"roomName":{"type":"string"},"language":{"type":"string"},"paths":{"type":"array","items":{"$ref":"#/components/schemas/Path"}}}},"Path":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"position":{"type":"integer","format":"int32"},"title":{"type":"string"},"description":{"type":"string"},"steps":{"type":"array","items":{"$ref":"#/components/schemas/Step"}}}},"Step":{"type":"object","properties":{"id":{"type":"string"},"position":{"type":"integer","format":"int32"},"title":{"type":"string"},"description":{"type":"string"},"imageAltText":{"type":"string"},"imageId":{"type":"string"},"imageUrl":{"type":"string"}}},"AIModuleActivatedResponse":{"type":"object","properties":{"activated":{"type":"boolean"}}},"GatewayMeteringRecord":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"tenantId":{"type":"string"},"gatewayId":{"type":"string"},"messageIdHex":{"type":"string"},"formatVersion":{"type":"integer","format":"int32"},"subAction":{"type":"integer","format":"int32"},"recordType":{"type":"string","enum":["FIVE_MINUTE","HOURLY_FALLBACK","CUMULATIVE_CHECKPOINT","CONSUMPTION_CHANGED"]},"sequence":{"type":"integer","format":"int64"},"intervalPulses":{"type":"integer","format":"int64"},"durationMs":{"type":"integer","format":"int64"},"totalPulses":{"type":"integer","format":"int64"},"stored":{"type":"boolean"},"cumulative":{"type":"boolean"},"receivedAt":{"type":"string","format":"date-time"}},"description":"An immutable metering record received from a gateway."},"PageGatewayMeteringRecord":{"type":"object","properties":{"totalPages":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"pageable":{"$ref":"#/components/schemas/PageableObject"},"size":{"type":"integer","format":"int32"},"content":{"type":"array","items":{"$ref":"#/components/schemas/GatewayMeteringRecord"}},"number":{"type":"integer","format":"int32"},"sort":{"$ref":"#/components/schemas/SortObject"},"numberOfElements":{"type":"integer","format":"int32"},"first":{"type":"boolean"},"last":{"type":"boolean"},"empty":{"type":"boolean"}}},"PageableObject":{"type":"object","properties":{"pageNumber":{"type":"integer","format":"int32"},"pageSize":{"type":"integer","format":"int32"},"offset":{"type":"integer","format":"int64"},"sort":{"$ref":"#/components/schemas/SortObject"},"paged":{"type":"boolean"},"unpaged":{"type":"boolean"}}},"SortObject":{"type":"object","properties":{"sorted":{"type":"boolean"},"empty":{"type":"boolean"},"unsorted":{"type":"boolean"}}},"QueuedDevicePackage":{"type":"object","properties":{"deviceId":{"type":"string","description":"Target device ID.","example":"849FAD3A11223344"},"queuedAt":{"type":"string","description":"Time at which the message entered the outbound queue.","format":"date-time"},"messageId":{"type":"string","description":"Existing queued message ID in hexadecimal.","example":"A1B2C3D4E5F6"},"action":{"type":"integer","description":"Protocol action value.","format":"int32","example":29},"subAction":{"type":"integer","description":"Protocol sub-action value.","format":"int32","example":5},"encrypted":{"type":"boolean","description":"True when the package is sealed with the device's active secure session. Action and sub-action above are the plaintext values. An encrypted package uses a send sequence and must be delivered promptly: the node rejects it once 32 newer frames have reached it."},"packageBase64":{"type":"string","description":"Complete 212-byte queued node-core package encoded as Base64. Decode and transmit unchanged."},"packageHex":{"type":"string","description":"The same complete 212-byte package encoded as 424 uppercase hexadecimal characters."}},"description":"Pending device queue message encoded for direct manual delivery. The Base64 and hexadecimal\nfields represent the same fixed 212-byte node-core message: message ID (6), little-endian\ntimestamp (2), action (1), sub-action (1), zero-padded data (201), and XOR checksum (1).\nPayloads may contain credentials, access codes, firmware, or other sensitive commands.\n"},"GatewayQueueMessage":{"type":"object","properties":{"nodeId":{"type":"string"},"createdDate":{"type":"string","format":"date-time"},"attemptedDeliveredDate":{"type":"string","format":"date-time"},"lastGatewayTried":{"type":"string"},"reasonLabel":{"type":"string"},"reason":{"type":"string"},"reasonDetails":{"type":"string"},"routeMode":{"type":"string"},"selectedGateway":{"type":"string"},"selectedGatewayState":{"type":"string"},"connectBlockReason":{"type":"string"},"eligibleGatewayCount":{"type":"integer","format":"int32"},"eligibleGateways":{"type":"array","items":{"type":"string"}},"messageId":{"type":"string"},"action":{"type":"integer","format":"int32"},"subaction":{"type":"integer","format":"int32"}}},"SolvotixEvent":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier","example":"abc123"},"userId":{"type":"string","description":"The id of the user who created this object.","example":"user456"},"createdDate":{"type":"string","description":"Date the object was created","format":"date-time","example":"2023-01-01T00:00:00Z"},"sensor_uuid":{"type":"string","description":"Sensor, gateway, room, booking, or other primary event target identifier.","example":"A1B2C3D4E5F60708"},"roomId":{"type":"string","description":"Room identifier when the event is associated with a room or booking.","example":"room-101"},"text":{"type":"string","description":"Primary event text when supplied by the producer."},"reason":{"type":"string","description":"Human-readable event reason. Do not parse this field when structured data is available."},"action":{"type":"integer","description":"Event action. Device protocol: 0 gateway online, 1 button pressed, 2 passive alarm, 3 active alarm, 4 high temperature, 5 unusual motion, 6 low battery, 7 power surge, 8 water leakage, 9 unauthorized access, 10 door left open, 11 ventilation anomaly, 12 freezing temperature, 13 high air pressure, 14 low temperature, 15 device online, 16 device offline, 17 gateway status, 18 test, 19 alarm cleared, 20 advertising-profile update, 21 firmware update, 22 firmware chunk, 23 firmware chunk complete, 24 restart node, 25 new node, 26 ask for time, 27 set time, 28 relay pulse milliseconds, 29 relay pulse seconds, 30 relay open, 31 relay close, 32 relay pulse minutes, 33 lock operation, 34 wall thermostat operation, 35 restart, 36 restart mode on, 37 restart mode off, 38 gateway ping/status, 39 gateway metering, 40 relay pulse count. Application events: 1024 room cleaned, 1025 room dirty, 1026 gateway online, 1027 gateway offline, 1028 smart-lock code added, 1029 smart-lock code removed, 1030 booking message sent, 1031 sensor restart mode on, 1032 sensor restart mode off, 1033 booking created, 1034 booking updated or checked out, 1035 booking deleted, 1036 lock automation queued, 1037 lock automation failed.","format":"int32","example":1034},"sub_action":{"type":"integer","description":"Action-specific sub-action. For action 33 (lock): 1 pulse open, 2 open, 3 close, 4 add codes, 5 remove codes, 6 set configuration, 7 delete all codes, 8 valid legacy PIN, 9 invalid legacy PIN, 10 pair devices, 11 fetch paired devices, 12 valid extended credential, 13 invalid extended credential, 14 add extended credential. Extended result data identifies the slot, validity, credential type, factor count, and presented credential factors. For action 34 (thermostat): 1 set Wi-Fi, 2 set target temperature, 3 turn on, 4 turn off, 5 restart, 6 delete Wi-Fi, 7 debug data. For action 39 (metering): 12 five-minute, 13 hourly, 14 total, 15 consumption changed. May be null for application events.","format":"int32","nullable":true,"example":8},"data":{"type":"object","additionalProperties":{"type":"string","description":"Structured string-valued metadata. Common keys are entityType, entityId, entityName, state, actorUserId, roomId, bookingId, code, slot, valid, factorCount, credentialType, credentialId, mifareUid, walletPlatform, triggerId, taskId, taskName, trigger, occurrence, channels, message, emailMessage, smsMessage, recipient, recipientEmail, and recipientPhone. Credential identifiers and codes are security-sensitive. State values depend on action; for action 1034 only state=checked_out proves a checkout transition, while state=updated is an ordinary update."},"description":"Structured string-valued metadata. Common keys are entityType, entityId, entityName, state, actorUserId, roomId, bookingId, code, slot, valid, factorCount, credentialType, credentialId, mifareUid, walletPlatform, triggerId, taskId, taskName, trigger, occurrence, channels, message, emailMessage, smsMessage, recipient, recipientEmail, and recipientPhone. Credential identifiers and codes are security-sensitive. State values depend on action; for action 1034 only state=checked_out proves a checkout transition, while state=updated is an ordinary update."},"timestamp":{"type":"integer","description":"Event timestamp as Unix seconds. Some device-originated historical events may use device time semantics.","format":"int32","example":1787306400},"nodeTime":{"type":"string","description":"Server-side time at which the event was stored.","format":"date-time","example":"2026-08-21T08:00:00Z"}},"description":"Tenant-scoped event. Actions 0-40 are device-protocol events; actions 1024-1037 are application events. Interpret sub_action in the context of action and use data.state for application-event state transitions."},"Page":{"type":"object","properties":{"totalPages":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"pageable":{"$ref":"#/components/schemas/PageableObject"},"size":{"type":"integer","format":"int32"},"content":{"type":"array","items":{"type":"object"}},"number":{"type":"integer","format":"int32"},"sort":{"$ref":"#/components/schemas/SortObject"},"numberOfElements":{"type":"integer","format":"int32"},"first":{"type":"boolean"},"last":{"type":"boolean"},"empty":{"type":"boolean"}}},"CodeHealth":{"type":"object","properties":{"code":{"type":"string"},"taken":{"type":"boolean"},"takenByBookingId":{"type":"string"},"valid":{"type":"boolean"},"uploadedToAllLocks":{"type":"boolean"},"uploadedLockCount":{"type":"integer","format":"int32"},"requiredLockCount":{"type":"integer","format":"int32"},"locksMissingCode":{"type":"array","description":"Ids of code-capable locks where this code is not confirmed as uploaded.","items":{"type":"string","description":"Ids of code-capable locks where this code is not confirmed as uploaded."}},"locks":{"type":"array","items":{"$ref":"#/components/schemas/LockHealth"}}}},"LockHealth":{"type":"object","properties":{"lockId":{"type":"string"},"lockName":{"type":"string"},"active":{"type":"boolean"},"lastHeardFrom":{"type":"string","format":"date-time"},"uploaded":{"type":"boolean"},"codeStatus":{"type":"string","description":"UPLOADED, MISSING, PENDING_UPLOAD, PENDING_REMOVAL, PENDING_CONFIRMATION, or UNKNOWN_STATE"},"slotNumber":{"type":"integer","format":"int32"},"slotState":{"type":"integer","format":"int32"},"uploadedToLockAt":{"type":"string","format":"date-time"}}},"RoomCodeHealthResponse":{"type":"object","properties":{"roomId":{"type":"string"},"roomName":{"type":"string"},"status":{"type":"string","description":"HEALTHY, DEGRADED, NO_CODES, or NO_CODE_CAPABLE_LOCKS"},"allCodesUploadedToAllLocks":{"type":"boolean"},"codeCount":{"type":"integer","format":"int32"},"codeCapableLockCount":{"type":"integer","format":"int32"},"unresolvedSensorIds":{"type":"array","description":"Assigned sensor ids that no longer resolve to a sensor.","items":{"type":"string","description":"Assigned sensor ids that no longer resolve to a sensor."}},"codes":{"type":"array","items":{"$ref":"#/components/schemas/CodeHealth"}}},"description":"Room-level health of stored access codes across every assigned code-capable lock."},"AutomationCapability":{"type":"object","properties":{"title":{"type":"string","description":"Category of the automation","example":"Temperature Control"},"actions":{"type":"array","description":"Actions carried out by this automation","items":{"type":"string","description":"Actions carried out by this automation"}}},"description":"Automation capability with its name and the actions it performs."},"AutomationMessage":{"type":"object","properties":{"key":{"type":"string","description":"Message key or trigger","example":"temperature_threshold"},"message":{"type":"string","description":"Human readable message sent when the trigger fires"}},"description":"Template for an automated notification emitted by building automation flows."},"AutomationTriggerDefinition":{"type":"object","properties":{"key":{"type":"string","description":"Trigger key","example":"on_day_of_arrival"},"description":{"type":"string","description":"Human readable description","example":"On day of arrival"},"requiresTimeOfDay":{"type":"boolean","description":"Whether the trigger requires a time of day"}},"description":"Supported automation trigger definition."},"AutomatedMessageLog":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"triggerId":{"type":"string"},"bookingId":{"type":"string"},"roomId":{"type":"string"},"recipientEmail":{"type":"string","description":"Recipient email used for delivery; sensitive personal data"},"recipientPhone":{"type":"string","description":"Recipient phone used for delivery; sensitive personal data"},"recipientKey":{"type":"string"},"localDate":{"type":"string"},"subject":{"type":"string"},"emailBody":{"type":"string","description":"Rendered email content sent to the recipient; sensitive"},"smsBody":{"type":"string","description":"Rendered SMS content sent to the recipient; sensitive"},"roomCode":{"type":"string","description":"Room code at dispatch time; security-sensitive"},"channels":{"type":"array","description":"Successfully processed delivery channels","example":["email","sms"],"items":{"type":"string","description":"Successfully processed delivery channels","example":"[\"email\",\"sms\"]"}},"createdAt":{"type":"string","format":"date-time"}},"description":"Successful automated-message delivery record for a booking recipient."},"LockAutomationTriggerDefinition":{"type":"object","properties":{"key":{"type":"string","description":"Trigger key stored on the task","example":"open"},"description":{"type":"string","description":"What the trigger does on locks, lock controllers and relays","example":"Unlocks and keeps the device open until a close trigger runs"}},"description":"A trigger a lock automation task can fire on its devices."},"AIChatThreadSummary":{"type":"object","properties":{"bookingId":{"type":"string","description":"Booking id connected to the thread.","example":"booking-123"},"userId":{"type":"string","description":"User id that created the thread.","example":"user-123"},"createdDate":{"type":"string","description":"When the thread was created.","format":"date-time"},"messageCount":{"type":"integer","description":"Number of messages in the thread.","format":"int32","example":4},"lastMessageText":{"type":"string","description":"Text from the latest message in the thread.","example":"Can you help me with check-in?"},"lastMessageRole":{"type":"string","description":"Role of the latest message sender.","example":"assistant"},"session_id":{"type":"string","description":"Session id of the AI chat thread.","example":"a6b8d9c0-3a0d-4d1d-8f69-8d6310de5d8a"}},"description":"Summary view of an AI chat thread."}},"securitySchemes":{"bearerAuth":{"type":"http","description":"Enter a Solvotix API token beginning with `sat_`, or a Firebase ID token.\n\nIn Swagger UI, enter only the token value. Swagger adds the `Bearer` prefix automatically.\n\nSolvotix API token example:\n\n```\nsat_<API_TOKEN>\n```\n\nThe resulting request header is:\n\n```\nAuthorization: Bearer sat_<API_TOKEN>\n```\n\nAPI tokens are tenant-bound and do not use Firebase. Firebase ID tokens remain supported\nfor interactive users.\n","scheme":"bearer","bearerFormat":"Solvotix API token (sat_...) or Firebase ID token"}}}}