{"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":"Guest Portal","description":"Endpoints for guest-facing booking and sensor controls. Booking-window guards use the room-code deactivation grace filter when configured, and reject checked-out bookings immediately. Feature toggles and room/device scope still apply."},{"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 Automation","description":"Scheduled opening, closing and standalone PIN changes for locks and controllers, plus\nmovement commands for 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 applied to each device in the task; code\ntriggers may complete as successful no-ops without queuing a command.\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| `add_code` | Adds the standalone numeric `code`; already present/pending is a successful no-op | Unsupported |\n| `remove_code` | Removes the standalone numeric `code`; already absent/pending is a successful no-op | Unsupported |\n\n**PIN requirements.** Code triggers accept a `code` string of 4-7 ASCII digits, preserving\nleading zeroes. It must match each device's existing PIN length. Only code-capable `lock_*`\ndevices are accepted; a relay anywhere in the list rejects the whole task write. PINs assigned\nto a Lock User on a target device are rejected, because reconciliation would restore them.\nValidation is repeated per device at execution, so later ownership or configuration changes\ncan fail an individual dispatch. Card and Wallet credentials are not supported by these triggers.\n\n`code` is included in authorized task responses for display on the task card; existing\nRestricted-role masking applies. `codeConfigured` indicates whether a PIN is stored.\nOn update, omit `code` or send null to retain the stored PIN. Supply the real\ndigits to replace it; masked values are invalid. Movement triggers must omit `code` or use null and clear\nthe stored PIN when selected. Add and remove at different times require separate tasks.\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\nA missed `remove_code` occurrence can leave a PIN active. A schedule is not a PIN expiry\nguarantee. Accepted code commands use the existing pending-slot recovery mechanism and may\nbe delivered later after a device reconnects; the catch-up window limits dispatch, not delivery.\nOpposite pending PIN commands are superseded in the server queue, and a removal of an\nunacknowledged add still queues a removal and retains its slot until acknowledgement. Late\nsuperseded acknowledgements do not overwrite the latest numeric-code command's slot state.\nCommands already sent to a gateway cannot be recalled; always verify the device.\n\nEvents 1036 (accepted dispatch or successful no-op) and 1037 (failed dispatch) include `taskId`,\n`taskName`, `trigger`, `occurrence`, `state`, and `actorUserId`, without the PIN. No-op states\nare `already_present`, `already_pending`, and `already_absent`; they have `queued=false` in\nmanual results. `already_pending` is not delivery confirmation. Verify acknowledged code\nchanges through code-slot state and device events 1028/1029, plus actual device behavior.\n\nThese endpoints change physical access. They are restricted to superusers.\n"},{"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":"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":"Dashboard","description":"SMS parts and message-send totals for the authenticated 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. Reconciles assigned lock credentials immediately: inactive, not-yet-valid or expired cards queue removal; currently valid cards queue enrollment. A five-minute monitor also reconciles validity. Offline locks retain credentials until removal is delivered; pass display expiry alone does not revoke lock access.","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/{id}/password":{"put":{"tags":["Users"],"summary":"Set a user's Firebase password","description":"Sets the actual Firebase Authentication password for an existing user in the selected\ntenant. Requires a bearer token or authenticated portal session and tenant context\nsupplied by the authentication filter (Tenant header for human users, or the API user's\nbound tenant when its Tenant header is omitted). Only full administrators\n(an empty role list) may call this operation. Restricted, User, Receptionist, Janitor,\nand Cleaning roles are forbidden. Unrestricted tenant-bound API users may also call it.\nHuman callers and the target user must belong to the selected tenant; API-user tenant\naccess is verified by the authentication filter. The target id is the Firebase uid\nreturned by the user-list operation. Existing users can be located in the portal via\nSettings > Administrators > Configure.\n\nSend a JSON body containing password, with at least six characters and not blank.\nWhitespace is preserved exactly. The old password is not required. This updates the\naccount globally, affecting login across every tenant the account belongs to.\nIt does not mark email as verified, enable a disabled account, or send an invitation.\n\nThis is security-sensitive: obtain explicit approval for the exact tenant, user and\naccount-wide credential change. Never log or echo the password. Success is synchronous:\n204 means Firebase accepted the password update. There is no command queue or dedicated\nevent. Passwords cannot be read back; the account owner can verify by signing in with the\nnew password. This operation does not guarantee immediate termination of existing sessions.\nRepeating the same request sets the same password, but do not retry automatically after an\nambiguous response because it could overwrite a newer password. Resolve with the account\nowner first. A 400 requires correcting the request or tenant context; a 401 requires valid\nauthentication; a 403 requires administrator access and tenant membership; a 404 means the\ntarget is not in the tenant or no longer exists in Firebase; a 502 means the Firebase update\nfailed and does not establish the final password state. No physical device operation occurs.\n","operationId":"setUserPassword","parameters":[{"name":"id","in":"path","description":"Firebase uid of a user in the selected tenant","required":true,"schema":{"type":"string"},"example":"u_001"}],"requestBody":{"description":"New account password. Do not log this request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPasswordRequest"},"example":{"password":"<NEW_PASSWORD>"}}},"required":true},"responses":{"204":{"description":"Firebase password updated; no response body"},"400":{"description":"Missing tenant context or invalid password update request"},"401":{"description":"Authentication is missing or invalid"},"403":{"description":"Caller is not a full administrator or cannot access the tenant"},"404":{"description":"Target user is not in the tenant or does not exist in Firebase"},"502":{"description":"Firebase password update failed; final state may be uncertain"}},"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 notification category toggles for the specified user in the current tenant. Requires a superuser bearer identity and Tenant header; tenant context is supplied by the authentication filter. sensorBatteryUnstable controls push and opted-in email warnings for explicit lock-reported motor-brownout faults (action 49), defaults to false when no preference exists, and is separate from sensorBatteryLow. Time requests, SET_TIME sends and code-use timing do not create battery warnings. Read-only; no device command or event is queued.","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 notification category toggles for the specified user in the current tenant. Requires a superuser bearer identity and Tenant header; tenant context is supplied by the authentication filter. Set sensorBatteryUnstable to true or false to enable or disable push and opted-in email warnings for explicit lock-reported motor-brownout faults (action 49), independently of sensorBatteryLow. Time requests, SET_TIME sends and code-use timing do not create battery warnings. Omitted or null sensorBatteryUnstable preserves the saved value, or defaults to false for a new preference. Existing category fields retain their full-update semantics: send their current values alongside the changed toggle. Returns the saved settings; read GET /api/users/pushnotification/settings/{userId} to verify. Saving settings does not send a warning, enqueue a device command or create a device event. Repeating the same complete settings is idempotent; after an ambiguous response read before retrying.","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,\nresolves per-device overrides and queues configuration only where effective settings change. Device overrides remain authoritative\nat boot and after tenant updates. Redundant overrides are pruned on save. Other tenant changes\ndo not queue device messages. Legacy device sound/system-code and Wallet NFC setup are\npreserved as sparse overrides when settings are first saved.\nwalletEnabled controls tenant Wallet card management. googleWalletReaderEnabled is the\nindependent default for Google Wallet readers; omitted updates preserve its stored value.\nIf unset, it falls back to walletEnabled for older clients. Per-device googleWalletEnabled\noverrides this reader default. Setup requires effective NFC enabled and S42 firmware 117+;\nNFC off blocks activation without erasing keys or assignments. Enabled saves recheck inactive\neligible readers while retaining pending work. Effective reader disable cancels setup and\nrequests disable, retaining keys/cards. Devices awaiting enrollment remain pending.\nWallet changes require unrestricted access; enabling requires protected Google configuration (503/409 on failure).\nA successful save confirms stored settings and asynchronous setup requests, not physical activation.\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"}}}},"403":{"description":"Wallet setup changes require unrestricted access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"409":{"description":"Wallet setup is not enabled in protected server configuration","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,\nresolves per-device overrides and queues configuration only where effective settings change. Device overrides remain authoritative\nat boot and after tenant updates. Redundant overrides are pruned on save. Other tenant changes\ndo not queue device messages. Legacy device sound/system-code and Wallet NFC setup are\npreserved as sparse overrides when settings are first saved.\nwalletEnabled controls tenant Wallet card management. googleWalletReaderEnabled is the\nindependent default for Google Wallet readers; omitted updates preserve its stored value.\nIf unset, it falls back to walletEnabled for older clients. Per-device googleWalletEnabled\noverrides this reader default. Setup requires effective NFC enabled and S42 firmware 117+;\nNFC off blocks activation without erasing keys or assignments. Enabled saves recheck inactive\neligible readers while retaining pending work. Effective reader disable cancels setup and\nrequests disable, retaining keys/cards. Devices awaiting enrollment remain pending.\nWallet changes require unrestricted access; enabling requires protected Google configuration (503/409 on failure).\nA successful save confirms stored settings and asynchronous setup requests, not physical activation.\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"}}}},"403":{"description":"Wallet setup changes require unrestricted access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"409":{"description":"Wallet setup is not enabled in protected server configuration","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":{"get":{"tags":["Smart Locks"],"summary":"Read smart lock defaults and device overrides","description":"Reads the authenticated tenant's defaults, sparse device overrides, effective desired\nsettings, device support flags and separately reported NFC/access/Wallet state. Does not\nqueue commands or write data. Missing reported fields mean unknown. systemCode is a\nsensitive credential; do not log or disclose it. Existing unrestricted/Restricted access\napplies. Read this resource before changing settings and after an ambiguous save. Tenant\ncontext is supplied by the authentication filter, without a tenant endpoint argument.\n","operationId":"getSmartLockConfiguration","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Configuration snapshot","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"400":{"description":"Missing tenant context or sensor is not a lock","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"403":{"description":"Access denied","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"404":{"description":"Sensor or tenant not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"503":{"description":"Legacy Wallet configuration unavailable","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}}},"security":[{"bearerAuth":[]}]},"put":{"tags":["Smart Locks"],"summary":"Set smart lock configuration","description":"Creates or updates persistent overrides for this tenant-owned lock. Use overrides with\nsoundLevel (off/low/medium/high), systemCode (six digits or empty to disable the menu),\nnfcEnabled (boolean, S42 only), accessMode (standard/openUntilClosed/forcedClosed), and\ngoogleWalletEnabled (boolean, S42 firmware 117+). Example:\n{\"overrides\":{\"nfcEnabled\":true,\"googleWalletEnabled\":true,\"accessMode\":\"standard\"}}.\nOmitted keys preserve overrides; explicit null removes a field override and restores\ninheritance. A value equal to the tenant default is removed instead of duplicated.\nUnchanged saves perform no database write or device command unless delivery is pending.\nGlobal changes apply only where the effective device setting changes; boot replies use\nthe same resolution. NFC disabled blocks Wallet activation without erasing keys/cards.\nWallet enablement requires unrestricted access and protected Google configuration;\nit does not enable tenant Wallet card management. Settings for unsupported device types\nare rejected. Room-code length remains a tenant setting.\nLegacy top-level soundLevel/soundEnabled/systemCode remain accepted and become overrides;\nlightEnabled/openSeconds/setTime remain accepted. Omitted legacy fields are preserved.\nDo not combine nested and top-level sound/system-code fields. setTime is retained as\nmetadata; configuration writes do not alter the node clock; boot replies still set time.\nAuthentication and tenant context come from the existing authentication filter; no tenant\nendpoint argument is accepted. This changes physical access policy: obtain operator\napproval, especially for forcedClosed or disabling readers. A 200 MessageFrame reports\ncommand acceptance; msg=null means no boot/legacy configuration command was needed\n(Wallet provisioning may still be asynchronous). GET this configuration to inspect\ndefaults, overrides, effective settings and separate reported status. Verify the existing\ndevice queue, reportedNfcEnabled/reportedAccessMode, walletNfc.google and event 1038,\nthen verify physical operation. Saving does not prove delivery or activation. Offline\ndevices retain their previous settings until delivery. After 409/503 or an ambiguous\nresponse read settings and queue before retrying; settings may already be saved. An\nidentical retry only requeues boot configuration when delivery was marked pending.\nThese backend settings will be available to the portal device configuration UI; no new\nfrontend navigation is introduced by this backend change.\n","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":"Settings saved; MessageFrame.msg is null when no boot/legacy command was needed","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"400":{"description":"Invalid settings, unsupported device settings, or missing tenant context","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"403":{"description":"Access denied or Wallet changes require unrestricted access","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"404":{"description":"Sensor or tenant not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"409":{"description":"Protected Wallet configuration disabled or settings saved but delivery pending","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"503":{"description":"Wallet configuration or Google service unavailable","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Smart Locks"],"summary":"Remove all smart lock overrides","description":"Removes all five device setting overrides and restores current tenant defaults. Never\ndeletes the device, keys, cards or assignments. Legacy light/open-duration/time metadata\nis retained. Restoring defaults may change physical access policy; obtain operator approval.\nUses the same authorization, asynchronous delivery, NFC/Wallet dependency and error\nsemantics as PUT configuration; unrestricted access is required for the Wallet reset.\nReturns the configuration snapshot, not proof of application. Repeat deletion is safe\nand queues nothing when effective settings are unchanged, unless delivery remains pending.\nInspect settings/queue after ambiguous responses, then verify reported device state,\nevent 1038 for Wallet work and physical operation. Tenant comes from authentication.\n","operationId":"resetSmartLockConfiguration","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Overrides removed; configuration snapshot returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"400":{"description":"Missing tenant context or invalid device","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"403":{"description":"Unrestricted access required for Wallet reset","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"404":{"description":"Sensor or tenant not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"409":{"description":"Protected Wallet configuration disabled or settings saved but delivery pending","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"503":{"description":"Wallet configuration unavailable","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}}},"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 default access mode inherited by keypad locks, lock controllers 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 default access mode and queues updated configuration only for devices\nwhose effective settings change; device overrides remain in effect. 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 local codes and cards on devices inheriting this default, 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. configuration.reportedNfcEnabled reports the NFC setting, and reportedAccessMode reports its access mode. Authenticated action-26 status v3 additionally supplies read-only reportedAndroidWalletActive and reportedAppleWalletActive booleans. Older reports omit the wallet flags, meaning unknown. Activation requires NFC enabled plus an activated reader profile; Apple VAS is currently unavailable. Eligible secure locks automatically receive the same action-48 status query as wallet_nfc_status when no provisioning request is pending. Read configuration.walletNfc.google and .apple for normalized status: boot flags do not replace merchant-key verification, an inactive Android report suppresses stale activation, and verified status results reconcile previously reported flags. The automatic query does not authorize setup, alter a pending request or activate keys. Read GET /api/smartlocks/{id}/configuration for tenant defaults, device overrides, effective desired settings and reported state.","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. The URL id selects the existing device; a body id is ignored. Firmware, device type, advertisement, lockSettingsOverrides, stored lock sound/system-code/light/open-duration/time settings, walletNfc and reported NFC/access-mode/Wallet activation fields are authoritative backend/device state and cannot be changed here. Returns the updated sensor entity, or 404 if the URL id is absent.\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. Also deletes backend encryption sessions, pending keys, replay/sequence state and enrollment caches, aborts backend OTA work, and cancels in-memory and persisted backend commands. 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 or encryption keys on the physical lock, queue a hardware reset, or remove room/lock-user references. Commands already sent to a gateway or device may still finish. No deletion event is emitted. Returns 204 with no body, including when the device is already absent; repeating deletion also cleans leftover code-slot, encryption and backend queue records. Verify absence through GET /api/sensor; reset hardware security separately before re-enrollment. 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, backend credentials, encryption state and backend commands deleted or already absent"},"500":{"description":"Cleanup failed; deletion may be partial, inspect ownership and inventory before retrying"},"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. Omitted or null mifareCredentials preserves existing cards; an explicit empty list removes them. Wallet assignments use their dedicated endpoints. All numeric PINs, including retained MIFARE PINs, must match the current tenant.roomCodeLength (4-7 digits; default 4). Configure it in Settings → Smart locks → Generated room code length. A mismatch returns 400 before saving or queueing; existing PINs are not resized automatically.","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. Omitted or null mifareCredentials preserves existing cards; an explicit empty list removes them. Wallet assignments use their dedicated endpoints. All numeric PINs, including retained MIFARE PINs, must match the current tenant.roomCodeLength (4-7 digits; default 4). Configure it in Settings → Smart locks → Generated room code length. A mismatch returns 400 before saving or queueing; existing PINs are not resized automatically.","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/bookings/{bookingId}/phone":{"put":{"tags":["Guest Portal"],"summary":"Save a missing guest phone number for a booking","description":"Use the booking ID from the guest portal link and Tenant header. No bearer token is required. The authentication filter supplies tenant context and accepts legacy tenantId query context when the header is absent. Requires askForContactDetailsConfirmation=true in tenant guest portal settings. Available before, during and after the stay, including checkout, independently of limitToValidTime. Submit JSON such as {\"phone\":\"+4512345678\"}. Accepts 7-15 digits with an optional leading +; spaces, parentheses, dots and hyphens are removed. Input is limited to 64 characters. Blank or invalid input returns 400; phone ownership is not verified. Only a missing or blank booking phone can be filled. An identical normalized phone is a successful no-op; a different existing number or conflicting concurrent update returns 409 and is never overwritten. This compatibility endpoint does not confirm contact details or mark new numbers as sync-protected. Use PUT /api/guest-portal/bookings/{bookingId}/contact-details to explicitly confirm both phone and email. Confirmed contacts cannot be changed here; a conflicting request returns 409. This does not add concurrency protection against a whole-booking sync save already in progress. HTTP 204 has no body and confirms the phone is stored locally. Missing tenant returns 400, disabled collection 403, unknown booking 404. This is a personal-data update requiring explicit guest submission. Safe to retry the same payload after a transient failure; do not retry a conflict with a different number. A successful write logs existing booking event 1034 with data.state=updated and guest actor; a no-op emits no event. The event includes existing booking metadata and phone; it does not prove phone verification or provider synchronization. No message, physical command or provider update is queued by this operation. Saving only a phone does not clear the contact confirmation prompt; confirm both contacts through contact-details. Use 204 as confirmation; event logging is separate from the phone write and may fail after it succeeds.","operationId":"saveGuestPortalPhoneNumber","parameters":[{"name":"Tenant","in":"header","description":"Tenant context; required unless supplied by the legacy tenantId query parameter","schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID from the guest portal link","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPhoneNumberRequest"}}},"required":true},"responses":{"204":{"description":"Phone saved or same normalized phone already present"},"400":{"description":"Missing tenant or invalid phone payload"},"403":{"description":"Contact confirmation prompt disabled"},"404":{"description":"Booking not found in tenant"},"409":{"description":"Phone already present or concurrent update conflict"}},"security":[]}},"/api/guest-portal/bookings/{bookingId}/contact-details":{"get":{"tags":["Guest Portal"],"summary":"Review guest contact details for a booking","description":"Use the booking ID from the guest portal link and Tenant header; no bearer token is required. The filter resolves tenant context and accepts legacy tenantId query context when the header is absent. Returns phone, email and contactDetailsConfirmed for guest review before explicit confirmation. Available before, during and after the stay, including checkout, independently of askForContactDetailsConfirmation and limitToValidTime. Read-only and idempotent, no approval required; safe to retry transient failures. Personal data must not be logged or cached. Verify directly from HTTP 200; no queue entry or event is produced. Missing tenant returns 400; unknown booking returns 404.","operationId":"getGuestPortalContactDetails","parameters":[{"name":"Tenant","in":"header","description":"Tenant context, resolved by the filter","schema":{"type":"string"}},{"name":"bookingId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Current contacts and confirmation state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestContactDetailsResponse"}}}},"400":{"description":"Missing tenant"},"404":{"description":"Booking not found"}},"security":[]},"put":{"tags":["Guest Portal"],"summary":"Save and confirm guest phone and email","description":"Use the booking ID from the guest portal link and Tenant header; no bearer token is required. The filter resolves tenant context and accepts legacy tenantId query context when the header is absent. After the guest reviews both values, submit phone, email and contactDetailsConfirmed=true. Both contacts are required. Phone accepts 7-15 digits with optional leading +; spaces, parentheses, dots and hyphens are removed, with at most 64 input characters. Email must be a single valid address of at most 254 characters; outer whitespace is trimmed. Available before, during and after the stay, including checkout, independently of askForContactDetailsConfirmation and limitToValidTime. Unconfirmed contacts may be corrected. Phone, email and confirmation are saved atomically. Later provider syncs preserve both confirmed values, even if the provider sends blank or different contacts. A whole-booking sync save already in progress can still overwrite a simultaneous confirmation. HTTP 204 confirms local persistence; identical confirmed submissions are no-ops. Different already-confirmed details or concurrent conflicts return 409. Missing tenant, invalid contacts or absent/false confirmation return 400; unknown booking returns 404. This personal-data mutation requires explicit guest confirmation, not an automatic submission by an agent. It is not email/phone ownership verification. Safe to retry the same payload after a transient failure; on 409 re-read contacts and do not silently replace confirmed data. Verify through GET contact-details and contactDetailsConfirmed=true. A write logs existing booking event 1034 with state=updated, guest actor and contact metadata; no-op retries emit no event. Event logging is separate from persistence and can fail after saving. No message, provider update or physical command is queued.","operationId":"confirmGuestPortalContactDetails","parameters":[{"name":"Tenant","in":"header","description":"Tenant context, resolved by the filter","schema":{"type":"string"}},{"name":"bookingId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestContactDetailsRequest"}}},"required":true},"responses":{"204":{"description":"Contacts saved and confirmed, or identical confirmed details already present"},"400":{"description":"Missing tenant, invalid contacts or confirmation not true"},"404":{"description":"Booking not found"},"409":{"description":"Contacts already confirmed with different values or concurrent conflict"}},"security":[]}},"/api/guest-portal-admin/settings":{"get":{"tags":["Guest Portal Admin"],"summary":"Get guest portal settings for current tenant","description":"Returns the authenticated tenant's settings, including generalMessageTemplate as the all-language fallback and generalMessages as language-specific overrides. Legacy settings return empty generalMessages and preStayMessages arrays. preStayInstructions is the pre-stay default; preStayMessages contains language-specific plain-text overrides.","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","description":"Updates the existing tenant guest portal configuration using an authenticated tenant session and Tenant header. Send the complete settings object, retaining existing messages, offers, languages and toggles. generalMessages contains language/message entries for general templates: omitted or null preserves saved translations; [] clears them; a nonempty array replaces them. Supported language codes are normalized like room messages; missing/unsupported languages, duplicate normalized languages, null entries and blank messages return 400 before settings are written. preStayMessages uses the same language/message validation and preserve/clear/replace semantics; it contains plain text without template expansion. Verify pre-stay overrides with admin GET and the existing booking display-settings GET using optional language, for an upcoming eligible booking. Messages are trimmed. Translations do not activate languages. The guest's requested active language selects a general template, falling back to generalMessageTemplate; room instructions are selected independently by room/language then room default, and inserted only through {roominstructions}. Existing booking variables remain supported. Verify saved translations with the settings GET and rendered output with GET /api/guest-portal/bookings/{bookingId}/messages?tenantId={tenantId}&language={language} for an eligible booking. Set preStayInstructions to plain text (for example, Check-in opens at 15:00), showPreStayInstructions to enable that text on the closed pre-stay screen, and askForContactDetailsConfirmation to prompt guests whose contact details are not yet confirmed, even when phone and email already exist. Both new toggles default to false when omitted; null instruction text clears it. Instructions are trimmed and no template variables are expanded. Pre-stay display also requires limitToValidTime=true and an upcoming, non-checked-out booking with valid dates. HTTP 200 returns saved settings; null payload returns 400. Verify with GET /api/guest-portal-admin/settings and the booking display-settings response. This changes guest-visible content and contact confirmation policy; obtain tenant approval for those choices. No physical operation, queue or event is triggered by these fields. Reapplying the same fields is safe, but re-read settings before retrying to avoid overwriting concurrent edits.","operationId":"updateGuestPortalSettings","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalSettings"}}},"required":true},"responses":{"400":{"description":"Missing settings payload or invalid generalMessages/preStayMessages entries. Correct input before retrying."},"200":{"description":"Persisted tenant settings, including the saved general-message overrides.","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","description":"Refreshes the cleaning snapshot and returns per-booking adults, children, numberOfGuests, plus numberOfGuests for currently occupied bookings in the room. A null count means unknown, not zero. showBookings=false hides booking details and the room count.","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. An explicit checkedOut=true to false transition clears the previous guest departure report, rollback fields, deferred cleaning marker and provider checkout-sync attempt state, allowing a fresh guest report when access is valid and checkout is enabled. An edit that keeps checkedOut unchanged preserves that state. The submitted start, end and checkedIn values still apply; reopening does not restore an earlier end or undo provider or device actions already sent. HTTP 200 confirms local persistence. Verify through the booking list and guest booking validity; normal access, automation and provider rules remain in effect.","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 {bookingId} for the original booking reference: the first nonblank value of Booking.externalId and Booking.bookingId, or empty text when neither is available or there is no booking. Supported in subjects, general/email/SMS bodies, inserted room instructions and manual booking-message overrides/previews. This display value does not change internal booking IDs or guest-portal URLs.  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. Optional byWeekdays restricts automatic sending to the send day's weekday in the tenant timezone (UTC if missing or invalid), independently of the booking's arrival weekday. Example: \"byWeekdays\":[\"MONDAY\",\"WEDNESDAY\",\"FRIDAY\"]. Full weekday names MONDAY through SUNDAY are accepted case-insensitively, trimmed and saved as unique uppercase values. Null or omitted means all days on create and preserves the selection on update; [] clears it. Null, blank or invalid entries return HTTP 400. Existing records without the field allow every day. This applies to guest and fixed-recipient automatic rules and all their configured channels, before contact, cleanliness, payment or delivery side effects. Excluded weekdays are outside the schedule and create no booking warning, including room-filter warnings. Booking lists suppress stale block reasons when no guest rule is currently due in the tenant timezone, and normalize legacy DAY_NOT_INCLUDED=6 to ALL_OK=0. A different due rule can still report its concrete failure; successful delivery clears the warning. Existing early-cleaning previews apply only on allowed days. No delivery attempt, successful log, event or new deferred queue entry is created for a weekday exclusion. Re-evaluation occurs about once per minute and on booking import/update within the existing trigger window: arrival/departure rules do not carry over beyond their local date; other rules can send on a later allowed day only while still eligible. Existing channel deduplication applies; this is not a daily repeat schedule. Manual sending and previews are unaffected; template-only rules never send automatically. Changing or clearing the selection can immediately make unsent bookings eligible; confirm the audience and content before saving. Verify the saved byWeekdays using GET /api/automation/messages/triggers, booking status using GET /api/bookings/bookings, and actual channels using GET /api/automation/messages/logs/bookings/{bookingId}. Provider receipt time can fall on another day; the filter controls initiation of automatic sending. Optional roomIds restricts booking-guest delivery to bookings whose roomId is listed, before checking contacts, cleanliness or payment. Example: \"roomIds\":[\"room-101\",\"room-102\"]. Null or omitted means all rooms on create and preserves the existing filter on update; [] clears it. Nonempty roomIds requires sendToBookingGuest=true; null or blank entries return HTTP 400. Use room identifiers, not room names or access codes. roomMessages only customizes content. A due booking excluded by room filters exposes automatedMessageBlockReason=5 (ROOM_NOT_INCLUDED) if no automated message has succeeded; a matching rule's failure takes precedence, and any successful delivery clears the warning. Verify messageSent and automatedMessageBlockReason using GET /api/bookings. Room filtering creates no delivery log, event or deferred queue entry. Existing eligibility windows and deduplication still apply; widening the room list can immediately send to eligible unsent bookings. 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\n`code` is the exception: omit it or send null for a code trigger to preserve its\nstored PIN. Send real digits to replace it; a masked PIN is rejected. Switching\nfrom a movement trigger to a code trigger requires a PIN. Switching to a movement\ntrigger clears the PIN and requires `code` to be omitted or null. The full device list is\nrevalidated for code support, compatible length and Lock User ownership.\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, device list or PIN; incompatible PIN length; Lock User-owned PIN; code supplied for a movement trigger","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. Packages omit QR codes and visible credential numbers while retaining NFC credentials, use the Solvotix navy palette and icon logo, and include usage instructions. For an already-existing Google card, refreshes its logo, navy background, display name, company name and usage instructions, explicitly removes the barcode and its alternate credential text and read-verifies the update while retaining all other fields and its Smart Tap credential. A missing Google object is inserted through the generated Google API and read-verified before signing a compact Save JWT containing only its object ID. A concurrent insert conflict is read-verified against the same tenant class and credential; no unrelated object is overwritten. Card artwork, instructions and NFC credentials stay in the Google object, not the URL. Saving the link associates this object with the phone; object creation alone does not prove installation. An existing object with a mismatched tenant class or credential returns 409 and is not overwritten. 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. Signing configuration is read only from /certsforsolvotixbackend/. Google requires a verified Smart Tap-enabled tenant class and redemption issuer; an absent class returns 503. Apple requires the operator-confirmed NFC signing entitlement and a Base64 P-256 SPKI encryption public key. Apple passes prohibit sharing and require authentication by default (configurable in the protected file). Package generation does not imply Apple VAS firmware support.","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, or an existing Google object identity conflicts","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, Google class verification 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/wallet-certificates/{id}/email":{"post":{"tags":["Wallet Certificates"],"summary":"Send Wallet card installation email","description":"Generates a fresh installation artifact for a tenant-owned card and submits one email through the existing tenant/default mail transport and branding. Apple attaches a signed .pkpass; Google sends a compact object-ID installation link valid for ten minutes; the card object is created or updated and verified through the generated Google API before link signing. Wallet emails disable SendGrid click tracking per message and on the installation anchor to avoid oversized tracking redirects. Packages omit QR codes and visible credential numbers, retain NFC credentials, use the Solvotix navy palette and icon logo, and include usage instructions. Before generating a Google link, updates an existing matching tenant card’s logo, navy background, display text and usage instructions, removes its QR code and visible credential number, and read-verifies the result without changing its Smart Tap credential. Body: {email: recipient@example.com}. Email must be one valid address of at most 254 characters, without display names, lists or line breaks. Does not update the user, card or assignments. SUBMITTED means SMTP acceptance, not inbox delivery, installation or lock activation; SUPPRESSED means development-mode recipient restrictions prevented sending. Non-idempotent: each explicit request can send another email. Do not automatically retry after a timeout or 502 because delivery may have occurred. Existing tenant SMTP timeout fallback can attempt the default sender once; duplicate delivery cannot be ruled out. Existing Wallet signing preconditions apply. No signing keys or installation links are returned or logged by this operation.","operationId":"sendWalletCardEmail","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletEmailRequest"}}},"required":true},"responses":{"200":{"description":"Mail transport submission or development-mode suppression result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletEmailResult"}}}},"400":{"description":"Invalid recipient email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletEmailResult"}}}},"401":{"description":"Authentication or tenant context rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletEmailResult"}}}},"404":{"description":"Wallet certificate not found in the authenticated tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletEmailResult"}}}},"409":{"description":"Wallet certificate is inactive or expired, or an existing Google object identity conflicts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletEmailResult"}}}},"502":{"description":"Mail submission could not be confirmed; delivery may have occurred","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletEmailResult"}}}},"503":{"description":"Wallet signing configuration or package generation unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletEmailResult"}}}}},"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\nWallet NFC readers use this same operation (operationId sensorAction). Set value to google or apple;\nnever supply keys, certificates, a Collector ID, or tenant identifiers in the request.\nActions: wallet_nfc_setup queries capability and then stages/activates the configured Google merchant key;\nwallet_nfc_status reads the authenticated lock configuration; wallet_nfc_activate enables a verified,\npropagated profile; wallet_nfc_disable retains keys; wallet_nfc_purge disables and erases local merchant\nkeys; wallet_nfc_rotate registers a replacement and starts setup on the tenant's previously enrolled locks;\nwallet_nfc_delete removes an inactive local profile (subAction is its decimal Google key version);\nwallet_nfc_revoke removes a retired Google public key after recent replacement verification across the fleet\n(subAction is its decimal key version). Revocation is a cloud operation and returns a MessageFrame with msg=null.\nWallet reader actions require unrestricted access (empty role list); Restricted identities receive 403.\nGoogle requires lock_s42 firmware 117+, active authenticated BLE, and an explicit experimental opt-in in\n/certsforsolvotixbackend/. Apple VAS is currently unsupported and returns 409.\nUse the existing generated sensorAction operation. In the portal, open Devices, the lock's three-dot menu,\nthen Wallet NFC for the configuration dialog. The dialog displays separate Google and Apple status;\nan active-wallet icon appears to the left of the secure-connection shield only after backend confirmation.\nSetup queues an NFC-only enable command for this lock before reading status, then authorizes asynchronous\nstaging and activation after the 24-hour Google propagation interval. It leaves the tenant-wide NFC\ndefaults unchanged and records sparse NFC/Google Wallet overrides for standalone setup.\nFor devices already managed by configuration CRUD, setup/activate returns 409 if effective\nNFC or Google Wallet is disabled; first update /api/smartlocks/{id}/configuration.\nBoot and tenant updates apply the same resolved settings. Disable/purge records Google\nWallet disabled without changing NFC power; rotation skips effectively disabled readers.\nSystem-menu option 7 remains a runtime operation. Use the updated firmware 117 build.\nA 200 response confirms queue acceptance only. Read GET /api/sensor/{id} configuration.walletNfc.google:\nmissing/unknown means unavailable and setupRequired=true; active=true requires a verified enabled profile\nplus reportedNfcEnabled=true. Do not write walletNfc or reported device settings through a sensor update.\nAuthenticated action-26 status v3 reports NFC at byte 1, Android Wallet activation at byte 3,\nand Apple Wallet activation at byte 4. The backend verifies these through the same action-48 status\npath when no request is pending. Reader status remains in configuration.walletNfc for existing clients;\ninactive Android reports suppress stale activation and Apple remains unsupported by this protocol.\nThis automatic status read does not authorize key setup or activation.\nAction 46 acknowledges transport; only 48/128 confirms provisioning. Events action 1038 contain state,\nwalletPlatform, requestId, generation, collectorId, keyVersion, fingerprint, errorCode, readyAt, apduStatusWord, rfMs, processingMs, and responseBytes when\navailable. Logged states include staged, activated, disabled, purged, key_deleted,\nfailed, public_key_revoked, and tap_diagnostic. Routine queued, status_confirmed, and unconfirmed\ntransitions are not written to the event log. Read configuration.walletNfc.google for reader status\nand observedAt to verify status reads. Optional scheduled merchant rotation is configured in the same protected files (off by default, 1-30 days); revocation remains an explicit operation. No private material is exposed.\nTap diagnostics (48/129, errorCode tap_status_0..7) do not prove access; inspect 33/12 accepted or 33/13 rejected.\nRequest IDs correlate provisioning results, not phone taps. After a lost reply, query wallet_nfc_status\nbefore retrying a mutation; a fresh logical retry uses a fresh message ID. Purge does not erase enrolled\ncredentials or revoke Google public keys. Disable/purge/revoke require explicit operator intent.\nWallet action validation errors return application/problem+json with status and the specific safe\nrejection reason in detail. Display detail instead of inferring the cause from HTTP 409 alone.\nAsynchronous device failures remain in configuration.walletNfc.google.errorCode and event 1038.\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"}}}},"403":{"description":"Wallet provisioning requires unrestricted tenant access","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MessageFrame"}}}},"409":{"description":"Wallet request rejected; detail identifies the failed precondition","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"503":{"description":"Protected wallet configuration or Google Wallet service is unavailable; detail identifies the reason","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}}},"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 compatible lock sensors while the card is active and inside its validity window. Future validFrom values defer enrollment until the five-minute reconciliation pass reaches that time. Inactive or expired cards return 409. Repeating the same assignment does not duplicate it; a 200 response does not prove upload or working NFC reader provisioning.","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":"Assignment stored; enrollment may be deferred to validFrom","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"400":{"description":"Invalid certificate identifier or request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"404":{"description":"Target or certificate not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"409":{"description":"Certificate is inactive or expired","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 compatible lock sensors while the card is active and inside its validity window. Future validFrom values defer enrollment until the five-minute reconciliation pass reaches that time. Inactive or expired cards return 409. Repeating the same assignment does not duplicate it; a 200 response does not prove upload or working NFC reader provisioning.","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":"Assignment stored; enrollment may be deferred to validFrom","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"400":{"description":"Invalid certificate identifier or request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"404":{"description":"Target or certificate not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockUser"}}}},"409":{"description":"Certificate is inactive or expired","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. Optional PINs must contain exactly tenant.roomCodeLength ASCII digits (4-7; default 4), configured in Settings → Smart locks → Generated room code length. A mismatch returns 400 before saving or queueing.","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 PIN does not match tenant.roomCodeLength","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. Optional PINs must contain exactly tenant.roomCodeLength ASCII digits (4-7; default 4), configured in Settings → Smart locks → Generated room code length. A mismatch returns 400 before saving or queueing.","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 PIN does not match tenant.roomCodeLength","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. Numeric PINs, including optional MIFARE PINs, must contain exactly tenant.roomCodeLength ASCII digits (4-7; default 4). Configure this in Settings → Smart locks → Generated room code length. Invalid PINs return 400 before saving or queueing.","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. Numeric PINs, including optional MIFARE PINs, must contain exactly tenant.roomCodeLength ASCII digits (4-7; default 4). Configure this in Settings → Smart locks → Generated room code length. Invalid PINs return 400 before saving or queueing.","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","description":"Requires an unchecked-out booking allowed by the room-code access window, including the configured deactivation grace period, showOpenButton, and a sensor in the assigned room. No bearer token is required in this booking-scoped guest workflow. HTTP success means accepted or queued, not physical completion. Require explicit guest intent; never automatically retry an ambiguous physical action. Disallowed access returns 403; unknown booking returns 404. Verify delivery through the existing command/event/device workflow.","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":"Report departure and schedule guest checkout in five minutes","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 an allowed booking access window, including configured room-code grace. HTTP 200 confirms a persisted departure report: checkoutSyncRequestedByGuest=true, the stored end and response checkoutTime are five minutes after the request, checkedOut stays false, and checkedIn is unchanged. The original checkout and request fields are saved for cancellation. The room's cleaning state stays unchanged until the lifecycle monitor completes checkout. Portal and room-code eligibility expire at this reported deadline, without adding grace days. At or after the deadline the lifecycle monitor atomically marks checkedOut=true and checkedIn=false only for guest-reported bookings. Its default interval is 60 seconds. Supported provider checkout for previously checked-in bookings is deferred until local checkout completes; transient failures are retried independently of the opt-in checkout_after_end filter. Inbound snapshots preserve the reported deadline and local status. Physical credential removal follows existing delivery rules and HTTP 200 is not physical completion. The report logs event 1034 with data.state=updated; monitor completion logs data.state=checked_out. Verify local status through booking validity and provider completion in the source provider. Requires explicit guest intent. Do not repeat after success or automatically retry an ambiguous response: repeated pending or completed reports return 403 without extending the deadline. After staff explicitly reopen a checked-out booking, its previous report and checkout-sync attempt state are cleared so a new eligible report can schedule a fresh five-minute deadline. Unknown booking or room returns 404; disabled checkout, missing room assignment, invalid booking window, or concurrent booking change 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}/checkout/cancel":{"post":{"tags":["Guest Portal"],"summary":"Cancel a pending reported guest checkout","description":"Booking-scoped Guest Portal operation with tenantId query context and no request body or bearer token. Use the booking ID from the guest link. Requires explicit guest intent, a pending departure report (checkoutSyncRequestedByGuest=true, checkedOut=false), a strictly future reported end, and a saved original end. It does not depend on the checkout button toggle after the report was accepted. The conditional update matches the reported deadline and original end so cancellation cannot reopen a completed checkout. HTTP 200 restores the original stored end, clears the guest report flag, restores the previous checkout request timestamp and previously-checked-in request marker, and removes the rollback snapshot. checkedIn and checkedOut remain unchanged. The response is BookingValidityResponse under the restored normal access/grace policy, with canCancelCheckout=false. The original end can already be past; cancellation does not guarantee continued access. Pending reports leave cleaning state unchanged, and provider checkout is not dispatched until completion, so cancellation queues no cleaning, device, or provider command. It does not undo completed provider checkout. Logs existing event 1034 with data.state=updated and guest actor; logging is separate from persistence. 400 means missing or blank tenant context; 404 means unknown booking; 409 means no cancellable report, expired deadline, missing legacy rollback data, or a concurrent booking change. Repeated cancellation returns 409. Do not automatically retry an ambiguous response; verify through GET booking validity, its restored checkoutTime, and canCancelCheckout=false. Restored access follows the original booking policy; cancellation requires explicit guest intent.","operationId":"cancelCheckout","parameters":[{"name":"tenantId","in":"query","description":"Tenant ID","required":true,"schema":{"type":"string"}},{"name":"bookingId","in":"path","description":"Booking ID from guest link","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancellation persisted; restored booking validity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingValidityResponse"}}}},"404":{"description":"Booking not found in tenant"},"400":{"description":"Missing or blank tenant context"},"409":{"description":"Report cannot be cancelled or booking changed"}}}},"/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 while the card is active and inside its validity window. Future validFrom values defer enrollment to reconciliation; inactive or expired certificates return 409. Repeating the same assignment does not duplicate it. A 200 response does not prove upload or working NFC reader provisioning.","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":"Assignment stored; enrollment may be deferred to validFrom","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomWalletCertificates"}}}},"400":{"description":"Invalid certificate identifier or request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomWalletCertificates"}}}},"404":{"description":"Target or certificate not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomWalletCertificates"}}}},"409":{"description":"Certificate is inactive or expired","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. A weekday outside a guest rule's schedule is not a failure. Current warnings are suppressed when no guest rule is due by tenant-local weekday, configured time and booking window; legacy DAY_NOT_INCLUDED=6 is normalized to ALL_OK=0. Existing early-cleaning previews apply only on allowed weekdays and active unchecked-out stays. A different due rule can still report its concrete block reason; successful delivery clears the warning. The response-only roomCodeActiveAfterCheckout is true when the stored checkout time has passed and any assigned room lock records the booking PIN as uploaded or pending removal with an upload timestamp, regardless of checkedIn, checkedOut, lock connectivity or grace period. It is false before the checkout time or when no presence is recorded; null means overdue lock state could not be resolved. The flag is not stored and cannot be set by clients. This status read does not queue a device action.","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 {bookingId} for the original booking reference: the first nonblank value of Booking.externalId and Booking.bookingId, or empty text when neither is available or there is no booking. Supported in subjects, general/email/SMS bodies, inserted room instructions and manual booking-message overrides/previews. This display value does not change internal booking IDs or guest-portal URLs.  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 {bookingId} for the original booking reference: the first nonblank value of Booking.externalId and Booking.bookingId, or empty text when neither is available or there is no booking. Supported in subjects, general/email/SMS bodies, inserted room instructions and manual booking-message overrides/previews. This display value does not change internal booking IDs or guest-portal URLs.  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, including byWeekdays. Null or empty byWeekdays allows every day; nonempty selections restrict automatic sending to those tenant-local send weekdays within the trigger's existing eligibility window. Manual sending is unaffected.","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 {bookingId} for the original booking reference: the first nonblank value of Booking.externalId and Booking.bookingId, or empty text when neither is available or there is no booking. Supported in subjects, general/email/SMS bodies, inserted room instructions and manual booking-message overrides/previews. This display value does not change internal booking IDs or guest-portal URLs.  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. Optional byWeekdays restricts automatic sending to the send day's weekday in the tenant timezone (UTC if missing or invalid), independently of the booking's arrival weekday. Example: \"byWeekdays\":[\"MONDAY\",\"WEDNESDAY\",\"FRIDAY\"]. Full weekday names MONDAY through SUNDAY are accepted case-insensitively, trimmed and saved as unique uppercase values. Null or omitted means all days on create and preserves the selection on update; [] clears it. Null, blank or invalid entries return HTTP 400. Existing records without the field allow every day. This applies to guest and fixed-recipient automatic rules and all their configured channels, before contact, cleanliness, payment or delivery side effects. Excluded weekdays are outside the schedule and create no booking warning, including room-filter warnings. Booking lists suppress stale block reasons when no guest rule is currently due in the tenant timezone, and normalize legacy DAY_NOT_INCLUDED=6 to ALL_OK=0. A different due rule can still report its concrete failure; successful delivery clears the warning. Existing early-cleaning previews apply only on allowed days. No delivery attempt, successful log, event or new deferred queue entry is created for a weekday exclusion. Re-evaluation occurs about once per minute and on booking import/update within the existing trigger window: arrival/departure rules do not carry over beyond their local date; other rules can send on a later allowed day only while still eligible. Existing channel deduplication applies; this is not a daily repeat schedule. Manual sending and previews are unaffected; template-only rules never send automatically. Changing or clearing the selection can immediately make unsent bookings eligible; confirm the audience and content before saving. Verify the saved byWeekdays using GET /api/automation/messages/triggers, booking status using GET /api/bookings/bookings, and actual channels using GET /api/automation/messages/logs/bookings/{bookingId}. Provider receipt time can fall on another day; the filter controls initiation of automatic sending. Optional roomIds restricts booking-guest delivery to bookings whose roomId is listed, before checking contacts, cleanliness or payment. Example: \"roomIds\":[\"room-101\",\"room-102\"]. Null or omitted means all rooms on create and preserves the existing filter on update; [] clears it. Nonempty roomIds requires sendToBookingGuest=true; null or blank entries return HTTP 400. Use room identifiers, not room names or access codes. roomMessages only customizes content. A due booking excluded by room filters exposes automatedMessageBlockReason=5 (ROOM_NOT_INCLUDED) if no automated message has succeeded; a matching rule's failure takes precedence, and any successful delivery clears the warning. Verify messageSent and automatedMessageBlockReason using GET /api/bookings. Room filtering creates no delivery log, event or deferred queue entry. Existing eligibility windows and deduplication still apply; widening the room list can immediately send to eligible unsent bookings. 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.\nThe stored PIN is returned in `code` for display on the task card, subject to\nexisting Restricted-role masking. `codeConfigured` is true when a PIN is stored.\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\nFor `add_code` or `remove_code`, supply a standalone `code` string of 4-7 ASCII\ndigits. Every device must support codes and use a compatible PIN length. A PIN\nassigned to a Lock User on any target device rejects the entire request. Movement\ntriggers must omit `code`. Responses include the stored PIN in `code` (subject to\nexisting Restricted-role masking) and the presence indicator `codeConfigured`.\n\nExample — add a standalone PIN every weekday at 08:00:\n\n```json\n{\n  \"name\": \"Enable staff PIN\",\n  \"trigger\": \"add_code\",\n  \"code\": \"012345\",\n  \"deviceIds\": [\"A1B2C3D4E5F60708\"],\n  \"schedule\": {\n    \"frequency\": \"WEEKLY\",\n    \"byWeekdays\": [\"MON\", \"TUE\", \"WED\", \"THU\", \"FRI\"],\n    \"timeOfDay\": \"08:00\",\n    \"startDate\": \"2026-11-05\",\n    \"timeZone\": \"Europe/Copenhagen\"\n  }\n}\n```\n\nCreate a second task with `trigger=remove_code`, the same real `code` string and\ndevices, and `timeOfDay=17:00` to revoke it later. Missed removals outside the\ncatch-up window are skipped; confirm removal through slots and device events.\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, device list or PIN; incompatible PIN length; Lock User-owned PIN; code supplied for a movement trigger","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\nCode triggers use the stored PIN without a request body. They revalidate ownership,\ndevice capability and PIN length per device. Successful no-ops return `queued=false`\nwith `status=already_present`, `already_pending`, or `already_absent` and event 1036.\nFailures include `managed_code`, `code_length_mismatch`, `slot_limit_reached`,\n`invalid_request`, `invalid_device`, `not_found`, and `failed`, with event 1037.\nA `200` may contain failures for every device. Neither `already_pending` nor\n`queued` proves delivery; verify slots and code-added/removed events 1028/1029.\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\nWallet reader status is normalized under configuration.walletNfc.google and .apple,\nwith supported, active, status and setupRequired. Authenticated action-26 status v3\nreports independent reportedNfcEnabled, reportedAndroidWalletActive and\nreportedAppleWalletActive fields. Eligible secure locks reconcile boot reports through\nthe same authenticated action-48 status verification as wallet_nfc_status; pending\nprovisioning requests are preserved. NFC and reader activation must both be confirmed\nfor active status, and Apple VAS remains unsupported by the current protocol.\nReading this inventory does not enqueue a status query or authorize key setup.\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","description":"Read-only booking-scoped guest operation; no bearer token or request body required. The booking must exist in the supplied tenant. Status 0 means upcoming, 1 means access allowed, and 2 means checked out, expired, or missing dates. With room_code_deactivation_grace_days configured, access follows the lock-code retention policy until end plus days of 24 hours; at the cutoff access is denied. Checked out always denies access, independent of showCheckoutButton. Guest-reported checkout expires at its stored five-minute deadline without adding grace days. Without the filter the original booking window applies. checkoutTime remains the stored departure; date is the opening time for status 0 and the effective access cutoff for status 1 or time-expired status 2. Use checkoutTime for the scheduled checkout countdown, including negative overdue time; use date only for access expiry. canCancelCheckout is true only while a reported deadline is future and rollback data exists. Reads are idempotent, retryable and need no approval. Verify through the 200 response; unknown booking returns 404. No queue/event is produced and access eligibility does not confirm physical lock state.","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":"Current booking access window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingValidityResponse"}}}},"404":{"description":"Booking not found in tenant"}}}},"/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","description":"For an eligible booking and activated language, returns one composed message. The general template uses the matching generalMessages language override, then generalMessageTemplate. Room instructions independently use the matching room/language entry, then the all-language room default, then empty text. Room instructions are inserted only at {roominstructions}; existing booking variables are expanded in both pieces. Response language is the normalized requested language even when default content is used. No bearer token is required within the existing booking-scoped workflow; supply tenantId and language. 400 indicates missing input, 404 an unresolved booking, missing room assignment or unavailable language, and 403 a disallowed booking window. Repeatable read; no physical command, guest communication or new event is produced. Do not log rendered content, which may contain access codes.","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 settings and pre-stay instructions for a booking","description":"No bearer token required; use the existing tenantId query context and a booking belonging to that tenant. contactDetailsConfirmed reports whether the guest confirmed phone and email. Available before, during and after the stay. HTTP 200 returns display settings; unknown booking returns 404. askForContactDetailsConfirmation is true only when enabled in tenant settings and contactDetailsConfirmed is false. Optional language selects a preStayMessages override only when that normalized language is activated. Missing, blank, unsupported, inactive or untranslated language falls back to preStayInstructions; a blank fallback yields null. Only the selected text is returned, never the translation list. Older clients may omit language. showPreStayInstructions is the configured toggle. preStayInstructions is plain text or null: text is returned only when that toggle and limitToValidTime are true, start and end exist with end after start, the booking has not started, is not checked out, and the text is nonblank. No placeholders are expanded. Render text without interpreting HTML. This does not authorize access to codes, messages or physical actions. Read-only, low risk, no approval, no request body, queue or event; idempotent and safe to retry. Verify through the 200 response, which uses Cache-Control: no-store. On failure hide optional content and contact confirmation prompts.","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"}},{"name":"language","in":"query","description":"Optional guest language for pre-stay instructions. Active-language match, otherwise default text.","required":false,"schema":{"type":"string"},"example":"de"}],"responses":{"200":{"description":"Guest display settings and eligible pre-stay text","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPortalDisplaySettingsResponse"}}}},"404":{"description":"Booking not found in tenant"}},"security":[]}},"/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, default to 40 events,\nand accept a size of 1-100. Optional inclusive `from` and `to` ISO-8601 timestamps must be supplied together.\nUse `sensorUuid`, `roomId`, or `bookingId` to filter the stream; these filters are mutually exclusive,\nand a room filter requires a time range. A supplied bookingId must not be blank; use Booking.bookingId\nrather than its database id. Direct matches use data.bookingId exactly. No current booking lookup is performed:\nretained events for deleted bookings remain readable, and no matching events returns an empty 200 page.\nExample: GET /api/events/page?bookingId=booking-123&page=0&size=40, with no request body.\nBooking results can include creation (1033), updates or checkout (1034), deletion (1035), sent messages\n(1030), and device events carrying that booking ID. Also includes otherwise unlinked credential-result\nand code add/remove events matching data.code found in any retained directly linked booking events.\nCode discovery is independent of pagination and optional from/to bounds. Inferred events must have\ncreatedDate within an inclusive start/end stay window recorded in those linked events. Missing/invalid\nstay windows disable code inference; events linked to another booking are excluded from inference.\nInference uses exact code matching within the tenant, with no device filter, and does not rewrite events\nor add data.bookingId. Code reuse within a recorded stay can still make inferred attribution ambiguous.\nFor lock action 33, sub-actions 8/12 report accepted\ncredentials and 9/13 rejected credentials; data.valid is a string, and credential metadata may be absent.\nFor action 1034, only data.state=checked_out proves a checkout transition; updated is an ordinary update.\nThese are recorded events, not a complete field-change audit or proof of physical entry. Other unlinked\nevents, integration request logs, and booking_history snapshots are not included. The union is deduplicated\nbefore database pagination and optional from/to bounds apply to both direct and inferred events.\nThis read has low risk, needs no approval, and creates no commands or queue entries.\nThe bearer token and required `Tenant` header provide tenant context. Requires Receptionist, Restricted,\nor unrestricted access (an empty role list). Receptionist results have access codes censored.\nOther event fields, including contacts, message bodies and credential identifiers, may be sensitive.\nVerify retrieval through content, totalElements and last; follow subsequent pages until last is true.\nSafe to retry with bounded backoff for transient failures. Correct 400 inputs and authentication/permissions\non 401/403 before retrying. A later page request reflects current data, so deduplicate event IDs when events\narrive; use fixed from/to bounds for a historical read and verify physical actions through device state.\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"}},{"name":"bookingId","in":"query","description":"Booking.bookingId for directly linked events plus unlinked access/code events matching recorded booking codes during recorded stay windows. Cannot be blank or combined with sensorUuid or roomId; time bounds are optional.","required":false,"schema":{"type":"string"},"example":"booking-123"}],"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"},"401":{"description":"Missing or invalid authentication"},"403":{"description":"Role does not permit reading event pages"},"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/dashboard":{"get":{"tags":["Dashboard"],"summary":"Get tenant dashboard message totals","description":"Returns SMS parts and email/WhatsApp sends for this month and last month from automated_message_logs, including manual booking sends recorded there. Authentication supplies the tenant context; no tenant path, query parameter or body is accepted. Calendar months use the tenant timezone with UTC fallback and include the start but exclude the next month's start. Each log containing a successful email or whatsapp channel counts once for that channel. SMS fields use tenant-scoped provider partCount for linked attempts when available, otherwise GSM-7 estimates (160 septets for one part, 153 per multipart part, extension characters occupy two septets) or Unicode estimates (70 UTF-16 units for one part, 67 per multipart part). Escape and surrogate pairs are not split across parts. Linked attempts count once within each calendar month; rejected attempts are excluded. A known provider attempt without partCount uses an estimate. Unresolved tracking uses one additional estimate per successful log, without multiplying by unknown retries. whatsappSentThisMonth and whatsappSentLastMonth count WhatsApp sends independently of SMS, using the same tenant-local month boundaries and createdAt timestamp. Failed-only WhatsApp attempts, receipt-only updates and sends outside automated_message_logs are excluded. Historical untracked SMS logs use estimates; part counts may change as receipts arrive. These are not exact supplier-invoice quantities. Failed channels and unlogged sends are excluded; these totals do not prove handset or inbox receipt or a WhatsApp read. Months without matching records return zero. Requires Receptionist, unrestricted or Restricted access. Read-only, idempotent and safe to retry after transient failure; no approval, device command, queue entry or event is produced. Verify against tenant delivery logs, provider part counts or the rendered SMS encoding and length, and createdAt timestamps.","operationId":"getDashboardData","responses":{"200":{"description":"Tenant-scoped SMS parts and email/WhatsApp send counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardData"}}}},"400":{"description":"Missing tenant context","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardData"}}}},"401":{"description":"Missing or invalid authentication or API-token tenant mismatch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardData"}}}},"403":{"description":"Role or tenant access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardData"}}}}},"security":[{"bearerAuth":[]}]}},"/api/cleaning/rooms":{"get":{"tags":["Cleaning"],"summary":"List rooms with cleaning status","description":"Returns the existing cleaning room snapshots with adult and child counts on each visible booking and numberOfGuests for currently occupied bookings per room. Unknown counts return null. Future arrivals retain their own counts without increasing the current occupancy total. showBookings=false hides booking details and the room count.","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 booking-message dispatch logs, newest first, including recorded failed attempts. Each record can include recipient contact details, rendered email/SMS content, processed channels, room code, and dispatch time. Email uses emailDeliveryStatus (ACCEPTED, FAILED, UNKNOWN), emailStatusUpdatedAt, emailErrorCode and emailErrorMessage separately from phone status. ACCEPTED means the SMTP sender completed, including any successful existing fallback; it does not confirm inbox delivery or reading. FAILED means definite rejection, authentication/validation failure or inability to connect. Timeouts, partial sends, failed timeout fallbacks and other unconfirmed errors are UNKNOWN. New failed email-only attempts are retained with email_failed or email_unknown markers; only ACCEPTED adds email to successful channels. Legacy email logs without saved outcomes are UNKNOWN with null timestamps/errors. When no email attempt is recorded, including a development skip, email status is null. Email failures do not prevent the configured phone attempt. No new resend mechanism or email delivery/read receipt is implemented. Use deliveryStatus, deliveryChannel, deliveryMessageId, deliveryStatusUpdatedAt, deliveredAt, readAt and deliveryAttempts for the same frontend display whether the phone message went by SMS or WhatsApp. Delivery attempts retain WhatsApp first and SMS fallback attempts separately. Aggregate priority is READ, DELIVERED, pending, UNKNOWN, failure; equal-priority outcomes prefer the last attempt. READ requires a recorded WhatsApp read receipt. Email is excluded from this aggregate. Local messaging-proxy failures are also retained: a refused connection, DNS/no-route failure or missing local configuration is FAILED before submission; timeouts, resets and unusable responses are UNKNOWN because sending may have occurred. deliveryErrorCode/deliveryErrorMessage and each attempt's errorCode/errorMessage expose safe diagnostics without internal URLs, credentials, recipient details or raw exception bodies. Provider message IDs and timestamps remain null when unavailable. An sms_failed marker is not a successful SMS channel. No phone attempt means null status and an empty deliveryAttempts array. WhatsApp IDs and send-result snapshots are retained internally for new dispatches; each read joins current tenant WhatsApp logs, so callbacks arriving after SMS fallback update this response. Historical WhatsApp sends without retained IDs are UNKNOWN; no lookup by recipient or approximate send time is attempted. Null receipt timestamps mean not recorded; a read time is never substituted for a delivery time. An sms channel means accepted for submission, not delivery to a handset. smsDeliveryStatus and smsDeliveryAttempts are read from saved provider receipts; DELIVERED is delivery confirmation, not a read receipt. smsMessageId, smsStatusCode, smsStatusUpdatedAt, smsDeliveredAt and smsPartCount describe the selected attempt. The SMS-only aggregate prefers any delivered attempt, then pending acceptance, then unknown, then failure. Historical SMS without tracking is UNKNOWN. Null smsDeliveryStatus means no tracked or logged SMS attempt. Provider callback capabilities and routing references are never returned. A provider receipt does not change booking messageSent/codeSent, trigger deduplication, dashboard counts, or generate new events/commands. An unknown booking id or a booking without logs returns an empty list. Re-read this endpoint for status updates; do not resend merely because delivery is pending or unknown.","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"}}}}},"500":{"description":"Message log or shared delivery-state storage unavailable; safe to retry this read","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.\nEach entry has `key`, `description`, and `requiresCode`. The two code triggers have\n`requiresCode=true` and require code-capable locks/controllers; relays are unsupported.\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}/configuration/{field}":{"delete":{"tags":["Smart Locks"],"summary":"Remove one smart lock override","description":"Restores inheritance for one field: soundLevel, systemCode, nfcEnabled, accessMode, or\ngoogleWalletEnabled. Equivalent to PUT configuration with that overrides key set to null.\nNever deletes the device. Returns defaults/overrides/effective/reported settings. Existing\nunrestricted/Restricted access applies; changes affecting Wallet setup require unrestricted\naccess. Operator approval, asynchronous delivery, retry/queue verification and error rules\nare the same as DELETE all overrides. Explicit NFC off blocks Wallet activation and retains\nkeys/cards. Authentication supplies tenant context. Repeating a reset is idempotent.\n","operationId":"resetSmartLockConfigurationField","parameters":[{"name":"id","in":"path","description":"Smart lock sensor ID","required":true,"schema":{"type":"string"}},{"name":"field","in":"path","description":"Setting to restore to its tenant default","required":true,"schema":{"type":"string","enum":["soundLevel","systemCode","nfcEnabled","accessMode","googleWalletEnabled"]}}],"responses":{"200":{"description":"Field override removed; configuration snapshot returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"400":{"description":"Unknown field, missing tenant or invalid device","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"403":{"description":"Access denied or Wallet change requires unrestricted access","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"404":{"description":"Sensor or tenant not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"409":{"description":"Protected Wallet configuration disabled or settings saved but delivery pending","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}},"503":{"description":"Wallet configuration unavailable","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SmartLockConfigurationResponse"}}}}},"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, including for an inactive or expired certificate, and queues removal from a lock only when no room or other lock user still grants the same wallet credential on that lock. Offline locks retain credentials until removal is delivered.","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, including for an inactive or expired certificate, and queues removal from a lock only when no room or other lock user still grants the same wallet credential on that lock. Offline locks retain credentials until removal is delivered.","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, including for an inactive or expired certificate, and queues removal from each room lock unless another room or lock user still grants it there. Offline locks retain credentials until removal is delivered.","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"},"UserPasswordRequest":{"required":["password"],"type":"object","properties":{"password":{"minLength":6,"type":"string","description":"New password, at least six characters and not blank. Preserved exactly, including whitespace. Never returned in a response.","format":"password","writeOnly":true}},"description":"Password to set on an existing Firebase Authentication account"},"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},"sensorBatteryUnstable":{"type":"boolean","description":"Enable warnings for explicit motor-brownout reports (action 49) from motorized battery locks, independently of low-battery alerts. Time requests, SET_TIME sends and code-use timing do not trigger battery warnings. Controls push and opted-in email in this tenant; fault reports remain logged as events when notifications are disabled. Defaults to false when no preference exists; omitted or null on update preserves the saved preference.","nullable":true,"default":false},"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 notification category settings for push and opted-in email in 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 inherited unless a device overrides it: 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":"Default node 10 NFC-reader setting inherited unless a device overrides it. Missing values default to false. Changes queue configuration only for devices whose effective settings change. Effective NFC off blocks Google Wallet activation without deleting keys or assignments.","example":false,"default":false},"walletEnabled":{"type":"boolean","description":"Tenant-wide Wallet card management. Missing values are disabled; omitted updates preserve the stored value. Also supplies the legacy Google reader default until googleWalletReaderEnabled is set. Device googleWalletEnabled overrides and effective NFC state determine reader setup/disable independently, without erasing keys or card assignments.","default":false},"googleWalletReaderEnabled":{"type":"boolean","description":"Default Google Wallet reader setting inherited by devices unless overridden. Omitted updates preserve the stored value; unset falls back to walletEnabled for compatibility. Independent of Wallet card management. Activation requires effective NFC enabled, S42 firmware 117+, authenticated enrollment and protected Google configuration."},"accessMode":{"type":"string","description":"Default access mode inherited unless overridden by 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. Backend administrator and booking email use 60-second socket timeouts and fall back once to the global sender on timeout unless delivery is already reported.","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 with tenant SMTP; backend administrator and booking email timeout fallback uses the configured global From address and preserves the original From as Reply-To","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"},"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"},"nrfVersion":{"type":"integer","format":"int32"},"gwMessage":{"type":"boolean"}}},"SmartLockConfigurationRequest":{"type":"object","properties":{"overrides":{"$ref":"#/components/schemas/SmartLockSettingsValues"},"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."},"SmartLockSettingsValues":{"type":"object","properties":{"soundLevel":{"type":"string","nullable":true,"enum":["off","low","medium","high"]},"systemCode":{"type":"string","description":"Six decimal digits, or an empty string to disable the physical system menu. Sensitive credential.","nullable":true},"nfcEnabled":{"type":"boolean","description":"NFC reader power setting. Supported on S42 locks.","nullable":true},"accessMode":{"type":"string","nullable":true,"enum":["standard","openUntilClosed","forcedClosed"]},"googleWalletEnabled":{"type":"boolean","description":"Google Wallet reader desired state, independent of tenant Wallet card management. Requires S42 firmware 117+, protected Google configuration and NFC enabled. NFC off blocks activation without erasing keys.","nullable":true}},"description":"Lock settings. In overrides, omitted fields are unchanged and explicit null restores inheritance. Effective/default values are read-only snapshots."},"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":"Default access mode inherited unless a device overrides it:\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 default smart lock access mode. The mode is inherited by keypad locks, lock controllers\nand wireless code panels 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","description":"Device configuration. lockSettingsOverrides contains sparse persistent overrides managed exclusively through /api/smartlocks/{id}/configuration; values equal to tenant defaults are omitted. Use that endpoint to read defaults and effective settings. Generic sensor updates cannot modify lock settings. reportedNfcEnabled and reportedAccessMode contain the node's reported settings. Authenticated action-26 status format v3 also reports independent booleans reportedAndroidWalletActive and reportedAppleWalletActive; older reports omit these fields (unknown, not false). A wallet active flag requires the NFC setting and an activated platform profile; Apple VAS remains unavailable in current firmware. These read-only runtime flags trigger the same authenticated action-48 status verification as wallet_nfc_status on an eligible secure lock, unless a provisioning request is already pending. A stale gateway encryption advertisement does not block a firmware-111+ message authenticated with the existing session; plaintext and replayed reports remain rejected. An inactive Android report suppresses stale active status; verified action-48 results reconcile previously reported Wallet flags. Boot reporting alone cannot confirm a merchant fingerprint or authorize setup. walletNfc.google and walletNfc.apple remain the read-only normalized reader status with supported, active, status and setupRequired. Missing status means supported=false, active=false, status=unknown, setupRequired=true. Apple is recorded as unsupported by the current protocol after an authenticated v3 report or correlated action-48 configuration result. Use wallet_nfc_* sensor actions to set up a reader; editing this map cannot activate it. Standalone Wallet setup records sparse NFC/Google Wallet desired overrides; for configured devices setup/activation respects effective settings, and boot replies apply those resolved settings."},"description":"Device configuration. lockSettingsOverrides contains sparse persistent overrides managed exclusively through /api/smartlocks/{id}/configuration; values equal to tenant defaults are omitted. Use that endpoint to read defaults and effective settings. Generic sensor updates cannot modify lock settings. reportedNfcEnabled and reportedAccessMode contain the node's reported settings. Authenticated action-26 status format v3 also reports independent booleans reportedAndroidWalletActive and reportedAppleWalletActive; older reports omit these fields (unknown, not false). A wallet active flag requires the NFC setting and an activated platform profile; Apple VAS remains unavailable in current firmware. These read-only runtime flags trigger the same authenticated action-48 status verification as wallet_nfc_status on an eligible secure lock, unless a provisioning request is already pending. A stale gateway encryption advertisement does not block a firmware-111+ message authenticated with the existing session; plaintext and replayed reports remain rejected. An inactive Android report suppresses stale active status; verified action-48 results reconcile previously reported Wallet flags. Boot reporting alone cannot confirm a merchant fingerprint or authorize setup. walletNfc.google and walletNfc.apple remain the read-only normalized reader status with supported, active, status and setupRequired. Missing status means supported=false, active=false, status=unknown, setupRequired=true. Apple is recorded as unsupported by the current protocol after an authenticated v3 report or correlated action-48 configuration result. Use wallet_nfc_* sensor actions to set up a reader; editing this map cannot activate it. Standalone Wallet setup records sparse NFC/Google Wallet desired overrides; for configured devices setup/activation respects effective settings, and boot replies apply those resolved settings."},"pairedDevices":{"type":"array","items":{"$ref":"#/components/schemas/SensorPairedDevice"}},"pushNotificationDelayMinutes":{"type":"integer","format":"int32"},"state":{"type":"string"},"relay":{"type":"boolean"},"uuid":{"type":"string"},"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},"activeAlarm":{"type":"boolean"},"gatewayWithBestRSSI":{"type":"string"},"codeLock":{"type":"boolean"},"wirelessCodePanel":{"type":"boolean"},"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":"Numeric access codes assigned to this user. On create/update each must contain exactly tenant.roomCodeLength ASCII digits (4-7; default 4). Leading zeros are preserved; existing codes are not automatically rewritten when the tenant setting changes.","items":{"type":"string","description":"Numeric access codes assigned to this user. On create/update each must contain exactly tenant.roomCodeLength ASCII digits (4-7; default 4). Leading zeros are preserved; existing codes are not automatically rewritten when the tenant setting changes."}},"mifareCredentials":{"type":"array","description":"MIFARE card credentials assigned to this user. Each credential may carry an optional PIN. Omitted or null on update preserves existing cards; an explicit empty list removes them.","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 numeric PIN matching tenant.roomCodeLength exactly (4-7 digits; default 4), 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)."},"GuestPhoneNumberRequest":{"required":["phone"],"type":"object","properties":{"phone":{"maxLength":64,"type":"string","description":"7-15 digits, optional leading +, with spaces, parentheses, dots or hyphens allowed; formatting is removed","example":"+4512345678"}},"description":"Guest-submitted phone number; fills only a missing booking phone and does not verify ownership"},"GuestContactDetailsRequest":{"required":["contactDetailsConfirmed","email","phone"],"type":"object","properties":{"phone":{"type":"string","description":"Required phone; same format as GuestPhoneNumberRequest","example":"+4512345678"},"email":{"maxLength":254,"type":"string","description":"Required single email address, trimmed","example":"guest@example.com"},"contactDetailsConfirmed":{"type":"boolean","description":"Must be true after the guest explicitly reviews and confirms both contacts"}}},"GuestPortalGeneralMessage":{"required":["language","message"],"type":"object","properties":{"language":{"type":"string","description":"Required supported language code, normalized like room-message languages. Only one entry per normalized language is allowed.","example":"de"},"message":{"type":"string","description":"Required nonblank template. Use {roominstructions} to insert the independently selected room message. Supports the existing booking variables.","example":"Willkommen! {roominstructions}"}},"description":"Language-specific guest portal general template. Applies to all rooms; does not activate its language."},"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."},"GuestPortalPreStayMessage":{"required":["language","message"],"type":"object","properties":{"language":{"type":"string","description":"Required supported language code, normalized like room-message languages; unique per normalized language.","example":"de"},"message":{"type":"string","description":"Required nonblank plain text for the closed pre-stay screen. Does not activate the language.","example":"Die Anreise ist ab 15:00 Uhr möglich."}},"description":"Language-specific plain-text pre-stay instructions. No HTML rendering or template-variable expansion."},"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},"askForContactDetailsConfirmation":{"type":"boolean","description":"Whether to ask guests to review and confirm phone and email when contactDetailsConfirmed is false. Defaults to false.","default":false},"showPreStayInstructions":{"type":"boolean","description":"Whether to show pre-stay instructions while limitToValidTime is true and the booking has not started. Defaults to false.","default":false},"preStayInstructions":{"type":"string","description":"Default plain-text instructions for the closed pre-stay screen when no active-language preStayMessages override matches. No template variables are expanded. Only exposed to guests when enabled, time-limited, and before a non-checked-out booking with valid dates starts. Blank text produces no instructions.","example":"Check-in opens at 15:00. Please contact reception if you arrive early."},"preStayMessages":{"type":"array","description":"Language-specific pre-stay instructions with language/message entries. Omitted or null on PUT preserves saved entries; [] clears; a nonempty array replaces all. Unsupported/missing languages, duplicate normalized languages, null entries and blank messages return 400. GET returns [] for legacy settings. Only an activated requested language selects an override; otherwise preStayInstructions is used. No template expansion; existing pre-stay visibility rules apply.","items":{"$ref":"#/components/schemas/GuestPortalPreStayMessage"}},"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":"Default general template when no generalMessages entry matches the requested language. Use {roominstructions} to insert room instructions; without it, only the general template is shown. Null or blank retains the existing built-in default behavior.","example":"Follow the room instructions: {roominstructions}."},"generalMessages":{"type":"array","description":"Language-specific general templates. Omitted or null on PUT preserves saved translations; an empty array clears all; a nonempty array replaces all. Entries require a supported language and nonblank message; duplicate normalized languages are rejected with 400. GET returns an empty array for legacy settings. Missing translations fall back to generalMessageTemplate. Does not activate languages.","items":{"$ref":"#/components/schemas/GuestPortalGeneralMessage"}}},"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"},"wifiMac":{"type":"string","description":"Wi-Fi station MAC reported by compatible gateway firmware, in uppercase colon-separated form. Null until a valid report is received; retained while offline and when older firmware reconnects. Bluetooth and access-point MAC addresses may differ.","nullable":true,"readOnly":true,"example":"AA:BB:CC:DD:EE:FF"},"wifiIp":{"type":"string","description":"Last gateway-reported local Wi-Fi IPv4 address, in dotted-decimal form. Null until a valid report is received; retained while offline and when older firmware reconnects. May change through DHCP and is not the public Internet address.","nullable":true,"readOnly":true,"example":"192.168.2.150"},"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"},"ready":{"type":"boolean"},"tenantOwner":{"type":"string"},"connected":{"type":"boolean"}}},"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"},"numberOfGuests":{"type":"integer","description":"Total adults and children in currently occupied bookings for this room. Zero when none are active; null if any active booking has an unknown count, the sum is invalid, or showBookings is disabled. Today's future arrivals are counted in bookings, not in this total.","format":"int32","nullable":true,"readOnly":true},"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"},"adults":{"type":"integer","description":"Adults assigned to this room booking; null when unknown.","format":"int32","nullable":true},"children":{"type":"integer","description":"Children assigned to this room booking; null when unknown or not allocated to a room.","format":"int32","nullable":true},"numberOfGuests":{"type":"integer","description":"Adult-plus-child count for this room booking; null when either count is unknown. An adult-only count is not returned as a complete total.","format":"int32","nullable":true},"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"},"contactDetailsConfirmed":{"type":"boolean","description":"Whether the guest explicitly confirmed phone and email. Confirmed values are preserved during later provider syncs; this is not ownership verification.","readOnly":true},"guestName":{"type":"string"},"adults":{"type":"integer","description":"Number of adults assigned to this room booking by the provider; null when unknown. Does not include children or repeat the reservation-wide guest count for every room.","format":"int32","nullable":true,"readOnly":true},"children":{"type":"integer","description":"Number of children assigned to this room booking; null when unknown or when reservation-wide children cannot be assigned to a specific room.","format":"int32","nullable":true,"readOnly":true},"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"},"roomCodeActiveAfterCheckout":{"type":"boolean","description":"Computed by the booking list: true only after the stored checkout time has passed and at least one assigned room lock records this booking code as uploaded or pending removal with a confirmed upload timestamp. Independent of checkedIn, checkedOut, connectivity and grace days. False before checkout time or when no code presence is recorded; null when overdue lock status cannot be determined, or in responses where this list-only field was not computed. Read-only and not persisted.","nullable":true,"readOnly":true},"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 credited as paid for the booking in the source system's currency. May include the full booking total when the provider explicitly reports prepayment."},"codeSent":{"type":"string","format":"date-time"},"externalReferenceIds":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}},"messageSent":{"type":"boolean","description":"Whether a booking-message log has at least one successful dispatch channel (email, sms or whatsapp). Failure-only, dev and empty-channel records do not count or clear dispatch warnings. An sms channel means provider acceptance, not handset delivery; inspect deliveryStatus in the booking-message log for receipts.","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, 5=ROOM_NOT_INCLUDED. Warnings are suppressed when no guest rule is due in the tenant timezone; legacy 6=DAY_NOT_INCLUDED is returned as 0. Existing allowed-day early-cleaning previews remain","format":"int32","readOnly":true,"example":0},"roomCode":{"type":"string","description":"Assigned room access code, retained on the booking after checkout and expiry regardless of the room-code grace filter or picker strategy. Eligible undo checkout can reload the same code. The lifecycle monitor clears the association seven days after the stored end time once access is inactive and room inventory and recorded lock slots no longer hold the credential. Retention does not imply guest-portal access. With the existing-code picker, cleanup releases a code for reuse instead of removing it from locks when rotation would reduce the confirmed uploaded pool below two codes. Room changes or an explicit code replacement can change the assignment."},"numberOfGuests":{"type":"integer","description":"Computed adult-plus-child count for this room booking. Null when either count is unknown, invalid or cannot be assigned to the room; never substitutes an adult-only count.","format":"int32","nullable":true,"readOnly":true}}},"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 {bookingId} for the original booking reference: the first nonblank value of Booking.externalId and Booking.bookingId, or empty text when neither is available or there is no booking. Supported in subjects, general/email/SMS bodies, inserted room instructions and manual booking-message overrides/previews. This display value does not change internal booking IDs or guest-portal URLs.  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)"},"roomIds":{"type":"array","description":"Optional room IDs restricting booking-guest delivery. Null or empty means all rooms; a nonempty list excludes bookings with no matching roomId. Independent of roomMessages content overrides.","example":["room-101","room-102"],"items":{"type":"string","description":"Optional room IDs restricting booking-guest delivery. Null or empty means all rooms; a nonempty list excludes bookings with no matching roomId. Independent of roomMessages content overrides.","example":"[\"room-101\",\"room-102\"]"}},"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"},"byWeekdays":{"type":"array","description":"Weekdays allowing automatic sending, evaluated on the send day in the tenant timezone (UTC when missing or invalid). Null or empty allows every day. Values are uppercase full weekday names MONDAY through SUNDAY. This filters existing trigger windows; it does not create daily repeats or extend eligibility. Manual sending and previews are unaffected.","example":["MONDAY","WEDNESDAY","FRIDAY"],"items":{"type":"string","description":"Weekdays allowing automatic sending, evaluated on the send day in the tenant timezone (UTC when missing or invalid). Null or empty allows every day. Values are uppercase full weekday names MONDAY through SUNDAY. This filters existing trigger windows; it does not create daily repeats or extend eligibility. Manual sending and previews are unaffected.","example":"[\"MONDAY\",\"WEDNESDAY\",\"FRIDAY\"]"}},"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. Automatic booking-guest messages wait for a room assignment when a deliverable channel uses {room}, {code}, {roominstructions}, or a legacy ROOM_SPECIFIC block, or when sendOnlyWhenRoomClean is true. The enabled email subject/body and SMS body are checked, using message as the body fallback. The whole rule waits without a delivery attempt, successful log, or dedicated block reason, and is re-evaluated within its existing trigger window. Plain confirmations can send without a room.  Use {bookingId} for the original booking reference: the first nonblank value of Booking.externalId and Booking.bookingId, or empty text when neither is available or there is no booking. Supported in subjects, general/email/SMS bodies, inserted room instructions and manual booking-message overrides/previews. This display value does not change internal booking IDs or guest-portal URLs.  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},"roomIds":{"type":"array","description":"Optional room IDs restricting automated booking-guest delivery; requires sendToBookingGuest=true. Omitted or null means all rooms on create and preserves the current filter on update. An empty list clears the filter. Entries must be nonblank room IDs, not room names or access codes. Nonmatching or missing booking roomId prevents sending before contact, cleaning and payment checks. This is independent of roomMessages content overrides.","example":["room-101","room-102"],"items":{"type":"string","description":"Optional room IDs restricting automated booking-guest delivery; requires sendToBookingGuest=true. Omitted or null means all rooms on create and preserves the current filter on update. An empty list clears the filter. Entries must be nonblank room IDs, not room names or access codes. Nonmatching or missing booking roomId prevents sending before contact, cleaning and payment checks. This is independent of roomMessages content overrides.","example":"[\"room-101\",\"room-102\"]"}},"byWeekdays":{"type":"array","description":"Optional weekdays allowing automatic sending on the send day in the tenant timezone. Accepts full weekday names MONDAY through SUNDAY, case-insensitive with surrounding whitespace trimmed; stores uppercase unique values. Null or omitted allows every day on create and preserves the saved selection on update. [] clears the restriction. Null, blank or invalid entries return HTTP 400. Existing trigger windows and channel deduplication apply; no daily repeat or extra retry window is created. Manual sending, previews and template-only rules are unaffected.","example":["MONDAY","WEDNESDAY","FRIDAY"],"items":{"type":"string","description":"Optional weekdays allowing automatic sending on the send day in the tenant timezone. Accepts full weekday names MONDAY through SUNDAY, case-insensitive with surrounding whitespace trimmed; stores uppercase unique values. Null or omitted allows every day on create and preserves the saved selection on update. [] clears the restriction. Null, blank or invalid entries return HTTP 400. Existing trigger windows and channel deduplication apply; no daily repeat or extra retry window is created. Manual sending, previews and template-only rules are unaffected.","example":"[\"MONDAY\",\"WEDNESDAY\",\"FRIDAY\"]"}}},"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 {bookingId} for the original booking reference: the first nonblank value of Booking.externalId and Booking.bookingId, or empty text when neither is available or there is no booking. Supported in subjects, general/email/SMS bodies, inserted room instructions and manual booking-message overrides/previews. This display value does not change internal booking IDs or guest-portal URLs.  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","add_code","remove_code"]},"code":{"pattern":"[0-9]{4,7}","type":"string","description":"Standalone numeric PIN for add_code/remove_code. Included in authorized task responses for display on the task card; existing Restricted-role masking applies. Omit on update to preserve the existing PIN; changing to a movement trigger clears it.","example":"012345"},"deviceIds":{"type":"array","description":"Sensor ids targeted by the trigger. Code triggers require code-capable locks/controllers; movement triggers also support relays.","items":{"type":"string","description":"Sensor ids targeted by the trigger. Code triggers require code-capable locks/controllers; movement triggers also support relays."}},"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"},"codeConfigured":{"type":"boolean","description":"Whether this task has a stored PIN.","readOnly":true}},"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","add_code","remove_code"]},"deviceIds":{"type":"array","description":"Target sensor ids; at least one is required. Code triggers require code-capable locks/controllers; movement triggers also support relays. Every device is validated before saving.","example":["A1B2C3D4E5F60708"],"items":{"type":"string","description":"Target sensor ids; at least one is required. Code triggers require code-capable locks/controllers; movement triggers also support relays. Every device is validated before saving.","example":"[\"A1B2C3D4E5F60708\"]"}},"schedule":{"$ref":"#/components/schemas/LockAutomationSchedule"},"enabled":{"type":"boolean","description":"Whether the task is active. Defaults to true when omitted.","example":true},"code":{"pattern":"[0-9]{4,7}","type":"string","description":"Standalone PIN of 4-7 ASCII digits, as a string preserving leading zeroes. Required for a new add_code/remove_code task. Omit or send null on update to retain its stored PIN; masked values are invalid. Must match each device's existing PIN length. Lock User-owned PINs and relays are rejected. Movement triggers must omit this field.","writeOnly":true,"example":"012345"}},"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."},"WalletEmailResult":{"type":"object","properties":{"status":{"type":"string","description":"SUBMITTED means accepted by the configured mail transport. SUPPRESSED means development mode prevented sending.","enum":["SUBMITTED","SUPPRESSED"]},"expiresAt":{"type":"string","description":"Expiration of the Google installation link, or null for an Apple attachment.","format":"date-time"}},"description":"Wallet email submission result. SMTP acceptance does not prove inbox delivery, phone installation or lock activation."},"WalletEmailRequest":{"required":["email"],"type":"object","properties":{"email":{"maxLength":254,"type":"string","description":"One recipient email address, without a display name or address list. Outer whitespace is trimmed; maximum 254 characters.","example":"recipient@example.com"}},"description":"Explicit recipient for a fresh Wallet installation email. This does not update the lock user's email or card assignments."},"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."},"ProblemDetail":{"type":"object","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer","format":"int32"},"detail":{"type":"string"},"instance":{"type":"string","format":"uri"},"properties":{"type":"object","additionalProperties":{"type":"object"}}}},"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","description":"Check-in state is unchanged when departure is reported"},"checkedOut":{"type":"boolean","description":"False when departure is accepted; the lifecycle monitor completes checkout after checkoutTime"},"checkInTime":{"type":"string"},"checkoutTime":{"type":"string","description":"Reported checkout deadline, five minutes after the request","format":"date-time"},"roomMarkedDirty":{"type":"boolean","description":"False when departure is reported; the room is marked dirty only after checkout completes"}}},"BookingValidityResponse":{"type":"object","properties":{"status":{"type":"integer","description":"0 upcoming, 1 access allowed, 2 checked out or expired","format":"int32","enum":[0,1,2]},"date":{"type":"string","description":"Opening instant for status 0; effective access cutoff for status 1 or time-expired status 2; otherwise null","format":"date-time"},"checkInTime":{"type":"string","description":"Stored booking arrival instant","format":"date-time"},"checkoutTime":{"type":"string","description":"Stored departure instant without room-code grace; after reporting departure this is the five-minute checkout deadline","format":"date-time"},"checkedIn":{"type":"boolean"},"checkedOut":{"type":"boolean"},"canCancelCheckout":{"type":"boolean","description":"True only for a pending guest-reported checkout with a strictly future deadline and saved original checkout time"}}},"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 {bookingId} for the original booking reference: the first nonblank value of Booking.externalId and Booking.bookingId, or empty text when neither is available or there is no booking. Supported in subjects, general/email/SMS bodies, inserted room instructions and manual booking-message overrides/previews. This display value does not change internal booking IDs or guest-portal URLs.  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":"Dispatch result. queued means a command was accepted. already_present, already_pending and already_absent are successful code no-ops with queued=false; already_pending does not confirm delivery. Failures: not_found, invalid_device, invalid_request, code_length_mismatch, managed_code, slot_limit_reached, failed.","enum":["queued","already_present","already_pending","already_absent","not_found","invalid_device","invalid_request","code_length_mismatch","managed_code","slot_limit_reached","failed"]}},"description":"One dispatch outcome for each configured device; code triggers can succeed without queuing a command"},"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","add_code","remove_code"]},"requestedAt":{"type":"string","description":"Server time when the manual run was requested","format":"date-time"},"devices":{"type":"array","description":"One dispatch outcome for each configured device; code triggers can succeed without queuing a command","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."},"SmartLockConfigurationResponse":{"type":"object","properties":{"deviceId":{"type":"string"},"defaults":{"$ref":"#/components/schemas/SmartLockSettingsValues"},"overrides":{"type":"object","properties":{"soundLevel":{"type":"string","nullable":true,"enum":["off","low","medium","high"]},"systemCode":{"type":"string","description":"Six decimal digits, or an empty string to disable the physical system menu. Sensitive credential.","nullable":true},"nfcEnabled":{"type":"boolean","description":"NFC reader power setting. Supported on S42 locks.","nullable":true},"accessMode":{"type":"string","nullable":true,"enum":["standard","openUntilClosed","forcedClosed"]},"googleWalletEnabled":{"type":"boolean","description":"Google Wallet reader desired state, independent of tenant Wallet card management. Requires S42 firmware 117+, protected Google configuration and NFC enabled. NFC off blocks activation without erasing keys.","nullable":true}},"description":"Lock settings. In overrides, omitted fields are unchanged and explicit null restores inheritance. Effective/default values are read-only snapshots."},"effective":{"$ref":"#/components/schemas/SmartLockSettingsValues"},"reported":{"type":"object","additionalProperties":{"type":"object","description":"Read-only device-reported settings. Missing fields mean unknown."},"description":"Read-only device-reported settings. Missing fields mean unknown."},"configurationDeliveryPending":{"type":"boolean","description":"True when the latest boot configuration could not enter the outbound queue. An identical configuration save retries queue acceptance. False does not prove device delivery."},"nfcSupported":{"type":"boolean"},"googleWalletSupported":{"type":"boolean"},"googleWalletBlockedByNfc":{"type":"boolean"}},"description":"Tenant defaults, sparse device overrides, effective desired settings and separately reported state. Saving/queueing does not prove physical application."},"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"}}},"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"},"askForContactDetailsConfirmation":{"type":"boolean","description":"Whether to prompt this guest: enabled by tenant and contactDetailsConfirmed is false, even when phone and email are already present"},"showPreStayInstructions":{"type":"boolean","description":"Configured tenant toggle; render instructions only when preStayInstructions is nonblank"},"preStayInstructions":{"type":"string","description":"Selected active-language pre-stay plain text, falling back to the default, only for an eligible closed pre-stay screen; otherwise null","nullable":true},"contactDetailsConfirmed":{"type":"boolean","description":"Whether the guest explicitly confirmed phone and email; not ownership verification"}}},"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"}}},"GuestContactDetailsResponse":{"type":"object","properties":{"phone":{"type":"string"},"email":{"type":"string"},"contactDetailsConfirmed":{"type":"boolean"}}},"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, 48 Google Smart Tap provisioning and tap diagnostics, 49 suspected motor-brownout restart (node types 7 and 10). 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 accepted or successful code no-op (state queued/already_present/already_pending/already_absent; taskId, taskName, trigger, occurrence, actorUserId; no PIN; not delivery proof), 1037 lock automation failed (producer status or failed, without PIN or exception text), 1038 wallet reader provisioning or tap diagnostic.","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 49 (motor brownout): 0, with no application payload. For action 39 (metering): 12 five-minute, 13 hourly, 14 total, 15 consumption changed. For action 1038 (wallet reader): 1 stage, 2 activate, 3 delete inactive profile, 4 status, 5 disable, 6 purge, 129 tap diagnostic, 0 public-key revocation. May be null for other 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, recipientPhone, requestId, generation, collectorId, keyVersion, fingerprint, errorCode, readyAt, apduStatusWord, rfMs, processingMs, and responseBytes. Action 49 uses state=suspected_motor_brownout and actorUserId=system; its reason explains that battery damage is suspected. Wallet-reader action 1038 uses walletPlatform=google; tap_status_0 is verified transfer only, not accepted access. 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, recipientPhone, requestId, generation, collectorId, keyVersion, fingerprint, errorCode, readyAt, apduStatusWord, rfMs, processingMs, and responseBytes. Action 49 uses state=suspected_motor_brownout and actorUserId=system; its reason explains that battery damage is suspected. Wallet-reader action 1038 uses walletPlatform=google; tap_status_0 is verified transfer only, not accepted access. 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 events, including action 49, retain wire timestamps or secure sequence numbers; use nodeTime or createdDate for server ingestion time.","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, 48 and 49 are device-protocol events; actions 1024-1038 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"}}},"DashboardData":{"type":"object","properties":{"smsSentThisMonth":{"type":"integer","description":"SMS parts this calendar month for logs containing the successful sms channel. Uses tenant-scoped provider partCount when retained; otherwise estimates GSM-7 parts at 160/153 septets or Unicode parts at 70/67 UTF-16 units. Linked provider attempts are deduplicated; rejected attempts are excluded. Not an exact supplier-invoice or delivery total.","format":"int64","example":42},"smsSentLastMonth":{"type":"integer","description":"SMS parts last calendar month, using the same provider-first and encoding-aware fallback calculation as smsSentThisMonth. Historical untracked messages use estimates.","format":"int64","example":80},"emailsSentThisMonth":{"type":"integer","description":"Logged email sends this calendar month; one per log containing the email channel","format":"int64","example":56},"emailsSentLastMonth":{"type":"integer","description":"Logged email sends last calendar month; one per log containing the email channel","format":"int64","example":95},"whatsappSentThisMonth":{"type":"integer","description":"Logged WhatsApp sends this calendar month; one per log containing the successful whatsapp channel, not delivery/read receipts or failed attempts","format":"int64","example":24},"whatsappSentLastMonth":{"type":"integer","description":"Logged WhatsApp sends last calendar month; one per log containing the successful whatsapp channel, not delivery/read receipts or failed attempts","format":"int64","example":38}},"description":"SMS parts and successful logged email and WhatsApp sends for the authenticated tenant. Calendar months use the tenant timezone, falling back to UTC. Missing usage returns zero."},"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":"Processed dispatch channels and any recorded failure markers. sms means provider acceptance, not handset delivery; inspect deliveryStatus for SMS/WhatsApp receipt state.","example":["email","sms"],"items":{"type":"string","description":"Processed dispatch channels and any recorded failure markers. sms means provider acceptance, not handset delivery; inspect deliveryStatus for SMS/WhatsApp receipt state.","example":"[\"email\",\"sms\"]"}},"emailDeliveryStatus":{"type":"string","description":"Email SMTP submission outcome, independent of phone deliveryStatus. ACCEPTED means submission completed, not inbox delivery or reading. FAILED means a definite submission failure. UNKNOWN means unconfirmed submission or a historical email without retained outcome. Null when no email submission was recorded, including development-mode skips.","enum":["ACCEPTED","FAILED","UNKNOWN"]},"emailStatusUpdatedAt":{"type":"string","description":"Local time the email submission outcome was recorded; null for historical untracked email logs","format":"date-time"},"emailErrorCode":{"type":"string","description":"Stable safe email submission error code; null for acceptance or historical untracked email"},"emailErrorMessage":{"type":"string","description":"Safe email submission explanation; excludes raw exception bodies, credentials, internal URLs and recipient details"},"deliveryStatus":{"type":"string","description":"Channel-neutral SMS/WhatsApp receipt status; use this for frontend delivery badges. READ is WhatsApp-only. Null for email-only/no phone attempt. Historical unlinked phone sends are UNKNOWN.","enum":["SUBMITTING","ACCEPTED","IN_TRANSIT","DELIVERED","READ","REJECTED","FAILED","EXPIRED","UNKNOWN"]},"deliveryChannel":{"type":"string","description":"Channel supplying the aggregate phone-delivery status; not necessarily the last channel submitted","enum":["sms","whatsapp"]},"deliveryMessageId":{"type":"string"},"deliveryStatusUpdatedAt":{"type":"string","format":"date-time"},"deliveryErrorCode":{"type":"string","description":"Stable local submission error code, when the selected phone attempt failed before a provider reference could be returned"},"deliveryErrorMessage":{"type":"string","description":"Safe submission failure description, excluding raw exception bodies, internal URLs, credentials and recipient details"},"deliveredAt":{"type":"string","description":"Known provider delivery timestamp for the selected channel. Null if not retained; read time is never substituted.","format":"date-time"},"readAt":{"type":"string","description":"Recorded WhatsApp read receipt time; null for SMS or when no read receipt exists","format":"date-time"},"deliveryAttempts":{"type":"array","description":"Phone-channel details: WhatsApp attempt first, then tracked SMS fallback attempts. Aggregate prefers READ, DELIVERED, pending, UNKNOWN, then failure. Email is excluded.","items":{"$ref":"#/components/schemas/MessageDeliveryAttempt"}},"smsDeliveryStatus":{"type":"string","description":"Aggregate SMS receipt/submission status, including local proxy failures without a provider ID. DELIVERED requires a provider receipt and does not prove reading. UNKNOWN includes ambiguous transport failures and historical/untracked SMS. Null when no SMS/proxy attempt was recorded.","enum":["SUBMITTING","ACCEPTED","IN_TRANSIT","DELIVERED","REJECTED","FAILED","EXPIRED","UNKNOWN"]},"smsMessageId":{"type":"string"},"smsStatusCode":{"type":"integer","format":"int32"},"smsStatusUpdatedAt":{"type":"string","format":"date-time"},"smsDeliveredAt":{"type":"string","format":"date-time"},"smsPartCount":{"type":"integer","description":"SMS parts reported by the provider, not the template estimate or legacy 160-character usage calculation","format":"int32"},"smsDeliveryAttempts":{"type":"array","description":"Submission/receipt state for each tracked provider attempt, in send order. Callback capabilities and cross-tenant routing IDs are never returned.","items":{"$ref":"#/components/schemas/SmsDeliveryAttempt"}},"createdAt":{"type":"string","format":"date-time"}},"description":"Booking message dispatch record. Use deliveryStatus for channel-neutral SMS/WhatsApp delivery and read receipts; smsDeliveryStatus remains SMS-only for compatibility. emailDeliveryStatus reports SMTP submission separately; email inbox delivery is not verified."},"MessageDeliveryAttempt":{"type":"object","properties":{"channel":{"type":"string","enum":["sms","whatsapp"]},"messageId":{"type":"string"},"status":{"type":"string","enum":["SUBMITTING","ACCEPTED","IN_TRANSIT","DELIVERED","READ","REJECTED","FAILED","EXPIRED","UNKNOWN"]},"statusCode":{"type":"integer","description":"Provider-specific status code; for WhatsApp only a recorded HTTP submission error is available, not a delivery error code","format":"int32"},"submittedAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"deliveredAt":{"type":"string","format":"date-time"},"readAt":{"type":"string","format":"date-time"},"partCount":{"type":"integer","description":"Provider-reported SMS parts; null for WhatsApp or pre-provider failures","format":"int32"},"errorCode":{"type":"string","description":"Stable safe local submission error code; null for provider receipts"},"errorMessage":{"type":"string","description":"Safe local failure description; no raw exception details or credentials"}},"description":"Channel-neutral phone-message attempt. Email is outside this receipt view. A null timestamp means it was not recorded; READ is available only from WhatsApp receipts."},"SmsDeliveryAttempt":{"type":"object","properties":{"messageId":{"type":"string"},"status":{"type":"string","enum":["SUBMITTING","ACCEPTED","IN_TRANSIT","DELIVERED","REJECTED","FAILED","EXPIRED","UNKNOWN"]},"statusCode":{"type":"integer","format":"int32"},"submittedAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"deliveredAt":{"type":"string","format":"date-time"},"partCount":{"type":"integer","format":"int32"}},"description":"Provider submission and receipt state for one SMS attempt; delivery is not a read receipt."},"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"},"requiresCode":{"type":"boolean","description":"Whether this trigger requires a standalone numeric PIN and code-capable devices; relays are unsupported when true"}},"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"}}}}