{"openapi":"3.1.0","info":{"title":"Maison API","description":"AI-powered hotel concierge platform API. Exposes REST, A2A (Agent-to-Agent), and MCP (Model Context Protocol) interfaces over the same business logic.","version":"1.0.0","contact":{"name":"Maison Labs","url":"https://maison-labs.com"}},"servers":[{"url":"https://maison-labs.com","description":"Production"}],"tags":[{"name":"REST API","description":"Chat sessions and client config"},{"name":"Client API","description":"Client-specific message translations and field configuration"},{"name":"Auth","description":"Session authentication"},{"name":"API Keys","description":"API key lifecycle management"},{"name":"Clients","description":"Client (tenant) CRUD management"},{"name":"A2A","description":"Agent-to-Agent protocol (JSON-RPC 2.0)"},{"name":"MCP","description":"Model Context Protocol (Streamable HTTP)"},{"name":"Guest MCP","description":"Guest-facing Model Context Protocol surface for external AI assistants. Unauthenticated — access is bounded by each hotel's guestapp entitlement intersected with its CHATGPT channel opt-in, with the GUEST_MCP_ENABLED environment gate as the outer bound — and its tool registry is disjoint from the operator MCP mount."},{"name":"Tools","description":"Website and integration diagnostic tools"},{"name":"Integrations","description":"Generic integration proxy — connect and manage external providers (ADR 0015). REST-only surface: these endpoints handle encrypted external-provider credentials and are not exposed via MCP tools or A2A methods, following the same precedent as client channels management."},{"name":"Diagnostic","description":"Agentic site diagnostic runs and follow-up chat"},{"name":"Webhooks","description":"Inbound integration callbacks from external platforms (Meta Instagram). These are transport-layer endpoints, exempt from A2A/MCP interface parity."},{"name":"Users","description":"User account management"},{"name":"Roles","description":"Role-based access control management"},{"name":"Features","description":"Feature flag and permission feature management"},{"name":"User Groups","description":"User group management and membership control"},{"name":"Settings","description":"Client-scoped settings: regions, departments, and tags"},{"name":"KB Gaps","description":"Detected knowledge-base gaps — guest questions the AI couldn't answer — and manual detection triggers"},{"name":"HubSpot","description":"Maison-internal HubSpot company mapping (MA-869)"},{"name":"Legacy","description":"Temporary backward-compatibility shims for legacy/business-console and services/pipeline-tasks. SCHEDULED FOR REMOVAL — do not build new clients against these endpoints. See docs/legacy-endpoints.md."}],"paths":{"/api/clients/{id}/integrations/rezstream/connectlink":{"post":{"tags":["Integrations"],"operationId":"sendRezstreamConnectLink","summary":"Email a rezStream connect invitation","description":"Emails a one-time invitation to the address stored on the client's rezStream integration row (config.emailId). rezStream consent can only be granted by the hotel's own rezStream account holder, who has no Maison login, so the operator sends them a link rather than completing the flow themselves. The recipient is always the stored address — never taken from the request. The invitation is single-use, expires after REZSTREAM_CONNECT_INVITE_TTL_SECONDS, and only its SHA-256 hash is persisted. Returns 404 if the client has no rezStream integration, 400 if no email address is saved on it, 503 if the rezStream connect environment is not configured. Requires update-client WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Invitation sent","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"sentTo":{"type":"string","description":"Address the invitation was sent to"}}}}}},"400":{"description":"No email address saved on the integration","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"No rezStream integration for this client","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to send rezStream connect invitation","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"rezStream connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/oauth/integrations/rezstream/start":{"get":{"tags":["Integrations"],"operationId":"startRezstreamIntegrationConnect","summary":"Redeem a rezStream connect invitation","description":"Redeems the one-time token from a rezStream connect invitation email and redirects the browser to rezStream's OAuth consent screen. Deliberately unauthenticated: the recipient is the hotel's rezStream account holder, who holds no Maison credential. Access is gated instead by the single-use, expiring, hashed invitation, bound to one client. This endpoint performs NO write — the invitation is burned by the callback once consent completes, so a mail-security prefetch cannot consume it. A fresh OAuth state is minted here at click time, so the callback's short validity window is unaffected by how long the email went unread. Every failure mode — unknown client, missing, expired, already redeemed, or wrong token — returns the same 400 so the endpoint cannot be used to probe which clients exist.","parameters":[{"name":"clientId","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID the invitation was issued for"},{"name":"token","in":"query","required":true,"schema":{"type":"string"},"description":"One-time invitation token from the email"}],"responses":{"302":{"description":"Redirect to the rezStream OAuth consent screen"},"400":{"description":"Invitation missing, expired, already used, or invalid","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to start rezStream connect","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"rezStream connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/oauth/integrations/rezstream/callback":{"get":{"tags":["Integrations"],"operationId":"rezstreamIntegrationCallback","summary":"Handle the rezStream OAuth callback for the integration proxy","description":"Receives the authorization code and state from rezStream, validates the HMAC-signed state, exchanges the code for tokens using the global REZSTREAM_CLIENT_SECRET, and writes the encrypted tokens to the client's integration row (marking it CONNECTED). Any outstanding connect invitation is burned here. Redirects to the console settings page on success. This is the redirect URI for the integration-proxy flow and is registered with rezStream per environment; it is distinct from the legacy PMS callback.","parameters":[{"name":"code","in":"query","required":true,"schema":{"type":"string"},"description":"Authorization code from rezStream"},{"name":"state","in":"query","required":true,"schema":{"type":"string"},"description":"HMAC-signed state parameter (binds client and issuing operator)"}],"responses":{"302":{"description":"Redirect to the console on success"},"400":{"description":"Missing or invalid code/state, or an OAuth error from rezStream","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to complete rezStream connect","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"Token exchange failed or returned no refresh token","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"rezStream connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/integrations/stscloud/connectlink":{"post":{"tags":["Integrations"],"operationId":"sendStscloudConnectLink","summary":"Email a one-time STS Cloud connect invitation","description":"Emails a single-use authorization link to the STS Cloud account holder recorded on the client's STS Cloud integration row (config.emailId) — the recipient always comes from stored config, never from the request. Only the SHA-256 hash of the token is persisted; the plaintext exists solely in the email. The invitation expires after STSCLOUD_CONNECT_INVITE_TTL_SECONDS and is burned when consent completes, not when the link is opened, so mail-scanner pre-fetching cannot consume it. Returns 404 if the client has no STS Cloud integration, 400 if no account holder email is saved on it, 409 if the per-hotel OAuth Client Secret has not been saved, 503 if the STS Cloud connect environment is not configured. Requires update-client WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Invitation sent","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"sentTo":{"type":"string","description":"Address the server actually sent to (the stored value)"}}}}}},"400":{"description":"Invalid client id, or no account holder email saved on the integration","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"No STS Cloud integration for this client","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"No STS Cloud OAuth Client Secret saved for this client","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to send STS Cloud connect invitation","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"STS Cloud connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/oauth/stscloud/start":{"get":{"tags":["Integrations"],"operationId":"startStscloudIntegrationConnect","summary":"Redeem an STS Cloud connect invitation","description":"Redeems the one-time token from an STS Cloud connect invitation email, mints a fresh HMAC-signed state carrying a PKCE nonce, and redirects the browser to STS Cloud's consent screen. Deliberately unauthenticated: the recipient is the hotel's STS Cloud account holder, who holds no Maison credential. Access is gated instead by the single-use, expiring, hashed invitation, bound to one client. This endpoint performs NO write — the invitation is burned by the callback once consent completes, so a mail-security prefetch cannot consume it. Every failure mode — unknown client, missing, expired, already redeemed, or wrong token — returns the same 400 so the endpoint cannot be used to probe which clients exist.","parameters":[{"name":"clientId","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID the invitation was issued for"},{"name":"token","in":"query","required":true,"schema":{"type":"string"},"description":"One-time invitation token from the email"}],"responses":{"302":{"description":"Redirect to the STS Cloud consent screen"},"400":{"description":"Invitation missing, expired, already used, or invalid","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to start STS Cloud connect","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"STS Cloud connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/oauth/stscloud/callback":{"get":{"tags":["Integrations"],"operationId":"stscloudIntegrationCallback","summary":"Handle the STS Cloud OAuth callback","description":"Receives the authorization code and state from STS Cloud, validates the HMAC-signed state, re-derives the PKCE code_verifier from the nonce it carries, and exchanges the code using the client's per-hotel OAuth Client Secret. Writes the encrypted access and refresh tokens to the client's integration row (marking it CONNECTED), burns the invitation that backed this authorization, and projects the tokens so the proxy picks them up immediately. Redirects to the console settings page on success. This path is the redirect URI registered with STS Cloud per environment.","parameters":[{"name":"code","in":"query","required":true,"schema":{"type":"string"},"description":"Authorization code from STS Cloud"},{"name":"state","in":"query","required":true,"schema":{"type":"string"},"description":"HMAC-signed state parameter (binds client, issuing operator, and the PKCE nonce)"}],"responses":{"302":{"description":"Redirect to the console on success"},"400":{"description":"Missing or invalid code/state, or an OAuth error from STS Cloud","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"No STS Cloud client secret stored for this client","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to complete STS Cloud connect","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"Token exchange failed or returned no refresh token","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"STS Cloud connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/translations":{"get":{"tags":["Client API"],"operationId":"getTranslations","summary":"Get client translations","description":"Fetch UI translations for a client. Keys and locales can be specified via headers or query params. Supports JSONP via the `tcb` query param: the response is then `<tcb>({\"translations\":…});` served as `application/javascript`, so `tcb` must be a dotted JavaScript identifier path (for example `mcbs.updateTranslations`) of at most 100 characters, given once. Any other value, including an empty or repeated `tcb`, is rejected with 400 before translations are fetched.","parameters":[{"name":"x-client-id","in":"header","required":false,"schema":{"type":"string"},"description":"Client (tenant) UUID"},{"name":"x-translation-keys","in":"header","schema":{"type":"string"},"description":"Comma-separated translation keys, or `*` for all"},{"name":"x-translation-locales","in":"header","schema":{"type":"string"},"description":"Comma-separated locales, or `*` for all"},{"name":"clientid","in":"query","schema":{"type":"string"}},{"name":"keys","in":"query","schema":{"type":"string"}},{"name":"locales","in":"query","schema":{"type":"string"}},{"name":"tcb","in":"query","schema":{"type":"string","pattern":"^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$","maxLength":100},"description":"JSONP callback: a dotted JavaScript identifier path such as `mcbs.updateTranslations`. Omit it to receive JSON."}],"responses":{"200":{"description":"Translations object: JSON, or a JSONP script calling `tcb` when that param is given","content":{"application/json":{"schema":{"type":"object"}},"application/javascript":{"schema":{"type":"string"}}}},"400":{"description":"Missing client ID or translation keys, locales given more than once, or a `tcb` that is not a dotted identifier path of at most 100 characters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to fetch translations","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/client/fields":{"get":{"tags":["Client API"],"operationId":"getClientFields","summary":"Get client config fields","description":"Fetch specific fields from a client's configuration.","parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"},"description":"Client (tenant) UUID"},{"name":"x-client-fields","in":"header","required":true,"schema":{"type":"string"},"description":"Comma-separated field paths (e.g. `hotelConfig.name,clientAssets.icon`)"},{"name":"x-client-locale","in":"header","schema":{"type":"string"},"description":"Locale override"}],"responses":{"200":{"description":"Requested fields","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing client ID or fields","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/me":{"get":{"tags":["Auth"],"operationId":"getMe","summary":"Get current user profile and permissions","description":"Returns the authenticated user's userId, email, and flattened permission set. Accepts session cookie or Authorization: Bearer header.","security":[{"sessionAuth":[]}],"responses":{"200":{"description":"Authenticated user profile","content":{"application/json":{"schema":{"type":"object","required":["userId","email","permissions","isMaisonAdmin"],"properties":{"userId":{"type":"string"},"email":{"type":"string"},"permissions":{"type":"array","items":{"type":"string"},"description":"Flattened permission strings: FEATURE:PERM or FEATURE:PERM:clientId"},"isMaisonAdmin":{"type":"boolean","description":"True when the user has global EXECUTE on ALL features (MaisonAdmin role)."}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to fetch user profile","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/translate":{"post":{"tags":["REST API"],"operationId":"translateText","summary":"Translate a short string for the chat widget","description":"Machine-translates one or more short strings into the language a guest is currently chatting in. Used by the chat widget's host-page automation API (`mcbs.sendMessage` / `mcbs.composeMessage`), where a hotel supplies a message in their own language and the guest may be conversing in another. Accepts either a single-item object or a batch of up to 10 items (all sharing one `targetLocale`). Requires the `x-client-id` header naming an existing client. Each item is limited to 1000 characters and counts individually against the rate limit. The endpoint answers 429 when the limit is exceeded. Returns the original text with `translated: false` when source and target resolve to the same language; on a per-item AWS failure the original text is returned for that item rather than failing the whole batch.","parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"},"description":"Client (tenant) UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"title":"Single","type":"object","required":["text","targetLocale"],"properties":{"text":{"type":"string","minLength":1,"maxLength":1000,"description":"The string to translate."},"targetLocale":{"type":"string","description":"Locale to translate into, e.g. `fi` or `fi-FI`. Region is ignored except where the written form differs (`zh-TW`, `pt-PT`, `es-MX`, `fr-CA`)."},"sourceLocale":{"type":"string","description":"Locale the text is written in. Omit to auto-detect."}}},{"title":"Batch","type":"object","required":["items","targetLocale"],"properties":{"items":{"type":"array","minItems":1,"maxItems":10,"description":"Up to 10 strings to translate, all into the same target locale.","items":{"type":"object","required":["text"],"properties":{"text":{"type":"string","minLength":1,"maxLength":1000,"description":"The string to translate."},"sourceLocale":{"type":"string","description":"Locale the text is written in. Omit to auto-detect."}}}},"targetLocale":{"type":"string","description":"Locale to translate all items into."}}}]}}}},"responses":{"200":{"description":"Translated text (or the original when no translation was needed). Single-item requests return a single object; batch requests return an `items` array.","content":{"application/json":{"schema":{"oneOf":[{"title":"Single response","type":"object","properties":{"text":{"type":"string"},"sourceLocale":{"type":"string"},"targetLocale":{"type":"string"},"translated":{"type":"boolean","description":"False when source and target are the same language."}}},{"title":"Batch response","type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"sourceLocale":{"type":"string"},"translated":{"type":"boolean","description":"False when source and target are the same language or when AWS failed for this item (original text is returned)."}}}}}}]}}}},"400":{"description":"Missing x-client-id, invalid body, or unsupported target locale","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"Translation provider unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/message":{"get":{"tags":["REST API"],"operationId":"getMessageSession","summary":"Get or create chat session","description":"Returns the current session (or creates one) for the given client.","parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"},"description":"Client (tenant) UUID"}],"responses":{"200":{"description":"Session with message history","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"messages":{"type":"array","items":{"$ref":"#/components/schemas/ChatMessage"}}}}}}},"400":{"description":"Invalid client ID or session","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Session does not exist","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["REST API"],"operationId":"postMessage","summary":"Send a chat message","description":"Submit a user query. Normally returns 202 Accepted immediately and delivers the assistant reply via the real-time events channel. Requests marked with the `x-test-session: true` header (the prompt-test / golden-FAQ worker, a backend job that is not a real-time subscriber) instead receive a synchronous 200 with the assistant reply in the body.","parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"},"description":"Client (tenant) UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryBody"}}}},"responses":{"200":{"description":"Synchronous assistant reply (test-session requests only — see the `x-test-session` header).","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"datetime":{"type":"number"},"type":{"type":"string"},"context":{"type":"array","items":{"type":"string"}},"selection":{"type":"string","description":"PROPERTY_SELECTION envelopes only: guest's stated property choice (free text)."},"pendingRequest":{"type":"string","description":"Guest's pending request carried on a PROPERTY_SELECTION reply."}}}}}},"202":{"description":"Query accepted for async processing","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ack"]},"sessionId":{"type":"string"}}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/auth/":{"get":{"tags":["Auth"],"operationId":"checkAuth","summary":"Check session authentication","description":"Returns 200 if the caller has a valid session; 401 otherwise. Returns user email for authenticated sessions.","security":[{"sessionAuth":[]}],"responses":{"200":{"description":"Authenticated — returns user info","content":{"application/json":{"schema":{"type":"object","properties":{"authenticated":{"type":"boolean"},"email":{"type":"string","format":"email"},"userId":{"type":"string"}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/auth/login":{"post":{"tags":["Auth"],"operationId":"login","summary":"Authenticate with credentials","description":"Authenticate with email and password. Auth payload must be base64(email:password) to avoid exposing the password in transit. On success, sets a session cookie scoped to .maison-labs.com.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["username","auth"],"properties":{"username":{"type":"string","format":"email","description":"User email address"},"auth":{"type":"string","description":"base64(email:password)"}}}}}},"responses":{"200":{"description":"Authenticated — session cookie set"},"400":{"description":"Missing username or auth","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"User must reset their password before logging in (admin-flagged force_reset_password); no session is issued.","content":{"application/json":{"schema":{"type":"object","required":["forceReset","email"],"properties":{"error":{"type":"string"},"forceReset":{"type":"boolean"},"email":{"type":"string","format":"email"}}}}}}}}},"/auth/logout":{"post":{"tags":["Auth"],"operationId":"logout","summary":"Log out and revoke session","description":"Clears the session cookie and revokes the Firebase refresh token. Always returns 200, even if token revocation fails.","responses":{"200":{"description":"Logged out — session cookie cleared"}}}},"/auth/resetpasswordcode":{"post":{"tags":["Auth"],"operationId":"requestResetPasswordCode","summary":"Request a TOTP password reset code via email","description":"Generates a stateless TOTP reset code and sends it to the user's registered email address. Always returns 200 regardless of whether the account exists (no account enumeration). Resend is throttled to one code per 30 seconds per account.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["nonce"],"properties":{"email":{"type":"string","format":"email","description":"Email address of the account to reset. Mutually exclusive with userId."},"userId":{"type":"string","format":"uuid","description":"UUID of the user to reset. Mutually exclusive with email."},"nonce":{"type":"string","minLength":16,"description":"Caller-generated random string (≥16 chars). Must be re-sent verbatim to /auth/resetpassword. Ties the code to this specific reset session."}}}}}},"responses":{"200":{"description":"Code sent (or silently skipped when account does not match any registration)","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"400":{"description":"Invalid request — e.g. both email and userId supplied","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"No identifier supplied and no active session","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/auth/resetpassword":{"post":{"tags":["Auth"],"operationId":"resetPasswordWithCode","summary":"Verify TOTP reset code and set a new password","description":"Verifies the 6-digit TOTP code (from /auth/resetpasswordcode) against the supplied nonce and sets the new password via Firebase Admin SDK. Verify attempts are rate-limited to 5 per minute per account+IP to prevent brute-force.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["nonce","code","newPassword"],"properties":{"email":{"type":"string","format":"email","description":"Email address of the account. Mutually exclusive with userId."},"userId":{"type":"string","format":"uuid","description":"UUID of the user. Mutually exclusive with email."},"nonce":{"type":"string","minLength":16,"description":"The same nonce sent to /auth/resetpasswordcode."},"code":{"type":"string","pattern":"^\\d{6}$","description":"6-digit TOTP code received via email."},"newPassword":{"type":"string","minLength":8,"description":"New password to set (minimum 8 characters)."}}}}}},"responses":{"200":{"description":"Password updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"400":{"description":"Invalid or expired code, or malformed request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Too many verify attempts — try again later","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/auth/users/{id}/invite/accept":{"post":{"tags":["Users","Auth"],"operationId":"acceptInvite","summary":"Accept invitation and create user account","description":"Verifies the invitation for the given user ID, sets the user's password via Firebase, and returns a session cookie. The invite link is single-use; subsequent calls return 400.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"UUID of the invited user"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password","first_name","last_name"],"properties":{"email":{"type":"string","format":"email","description":"User email address (must match the invitation)"},"password":{"type":"string","description":"Password to set for the new account"},"first_name":{"type":"string","description":"User's first name"},"last_name":{"type":"string","description":"User's last name"}}}}}},"responses":{"200":{"description":"Invitation accepted — session cookie set (HttpOnly, Secure, SameSite=Lax)","content":{"application/json":{"schema":{"type":"object","required":["session","email"],"properties":{"session":{"type":"string","description":"Session token"},"email":{"type":"string","format":"email"}}}}}},"400":{"description":"Invalid or already-accepted invitation","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Invited user not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Account already exists for this email","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/user/apikey":{"put":{"tags":["API Keys"],"operationId":"createApiKey","summary":"Create API key","description":"Generate a new HMAC-signed API key. The full key is returned once and cannot be recovered. Requires session auth (API key auth is rejected).","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-user-id","in":"header","schema":{"type":"string"},"description":"Target user UUID. If omitted, creates for self. Requires ApiKeys WRITE permission for other users."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateBody"}}}},"responses":{"201":{"description":"API key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"API key auth used, or insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Target user not found or disabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/user/apikey/{id}":{"delete":{"tags":["API Keys"],"operationId":"deleteApiKey","summary":"Disable API key","description":"Disable an API key by its database ID. Requires session auth. Deleting another user's key requires ApiKeys WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"API key database UUID"}],"responses":{"200":{"description":"API key disabled","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"API key auth used, or insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"API key not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/.well-known/agent.json":{"get":{"tags":["A2A"],"operationId":"getAgentCard","summary":"A2A agent card","description":"Discovery endpoint for the A2A protocol. Returns capabilities, skills, and auth requirements.","responses":{"200":{"description":"Agent card","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentCard"}}}}}}},"/a2a":{"post":{"tags":["A2A"],"operationId":"a2aDispatch","summary":"A2A JSON-RPC endpoint","description":"JSON-RPC 2.0 endpoint for A2A protocol. Methods: `tasks/send`, `tasks/get`, `tasks/cancel`, plus management methods including `hubspot/searchcompanies`, `hubspot/getmapping`, `hubspot/setmapping`, `hubspot/removemapping`, `hubspot/pushchatvolume`, `hubspot/createcompany`. Requires API key + client ID.","security":[{"apiKey":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"},"description":"Client (tenant) UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcRequest"}}}},"responses":{"200":{"description":"JSON-RPC response (success or error)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcResponse"}}}}}}},"/mcp":{"post":{"tags":["MCP"],"operationId":"mcpPost","summary":"MCP request handler","description":"Streamable HTTP transport for MCP. Handles initialize, tools/list, tools/call, and other JSON-RPC messages. Include `mcp-session-id` header for established sessions.","security":[{"apiKey":[]}],"parameters":[{"name":"mcp-session-id","in":"header","schema":{"type":"string"},"description":"Session ID returned from initialize. Omit for the initial request."}],"requestBody":{"required":true,"description":"JSON-RPC 2.0 message. For `tools/call`, the `params.arguments` object conforms to one of the MCP tool schemas (see components/schemas for Mcp*Params). Core schemas: McpQueryHotelParams, McpGetSessionParams, McpCreateSessionParams, McpGetClientInfoParams, McpListTranslationsParams. Management schemas: McpListClientsParams, McpGetClientParams, McpCreateClientParams, McpUpdateClientParams, McpDisableClientParams, McpListUsersParams, McpGetUserParams, McpInviteUserParams, McpUpdateUserParams, McpDisableUserParams, McpReinviteUserParams, McpForceResetUserParams, McpGetUserActivityParams, McpListRolesParams, McpGetRoleParams, McpCreateRoleParams, McpUpdateRoleParams, McpDisableRoleParams, McpAddRolePolicyParams, McpRemoveRolePolicyParams, McpGetRoleUsersParams, McpGetRoleGroupsParams, McpListFeaturesParams, McpCreateFeatureParams, McpUpdateFeatureParams, McpDeprecateFeatureParams, McpListUserGroupsParams, McpGetUserGroupParams, McpCreateUserGroupParams, McpUpdateUserGroupParams, McpDeleteUserGroupParams, McpUpdateGroupMembersParams, McpUpdateGroupRolesParams, McpListRegionsParams, McpCreateRegionParams, McpUpdateRegionParams, McpDeleteRegionParams, McpListDepartmentsParams, McpCreateDepartmentParams, McpUpdateDepartmentParams, McpDeleteDepartmentParams, McpListTagsParams, McpCreateTagParams, McpUpdateTagParams, McpDeleteTagParams, McpListClientPipelineTasksParams, McpStopClientPipelineTaskParams. Knowledge-gap schemas: McpListKnowledgeGapsParams, McpRunKnowledgeGapDetectionParams. Diagnostic schemas: McpDiagnosticStartParams, McpDiagnosticGetSessionParams, McpDiagnosticChatParams. HubSpot schemas: McpSearchHubspotCompaniesParams, McpGetHubspotCompanyMappingParams, McpSetHubspotCompanyMappingParams, McpRemoveHubspotCompanyMappingParams, McpPushHubspotChatVolumeParams, McpCreateHubspotCompanyParams.","content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","id","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"oneOf":[{"type":"string"},{"type":"number"}]},"method":{"type":"string","description":"MCP method (e.g. initialize, tools/list, tools/call)"},"params":{"type":"object","properties":{"name":{"type":"string","description":"Tool name (for tools/call)","enum":["query_hotel","get_session","create_session","get_client_info","list_translations"]},"arguments":{"oneOf":[{"$ref":"#/components/schemas/McpQueryHotelParams"},{"$ref":"#/components/schemas/McpGetSessionParams"},{"$ref":"#/components/schemas/McpCreateSessionParams"},{"$ref":"#/components/schemas/McpGetClientInfoParams"},{"$ref":"#/components/schemas/McpListTranslationsParams"},{"$ref":"#/components/schemas/McpListClientsParams"},{"$ref":"#/components/schemas/McpGetClientParams"},{"$ref":"#/components/schemas/McpCreateClientParams"},{"$ref":"#/components/schemas/McpUpdateClientParams"},{"$ref":"#/components/schemas/McpDisableClientParams"},{"$ref":"#/components/schemas/McpListUsersParams"},{"$ref":"#/components/schemas/McpGetUserParams"},{"$ref":"#/components/schemas/McpInviteUserParams"},{"$ref":"#/components/schemas/McpUpdateUserParams"},{"$ref":"#/components/schemas/McpDisableUserParams"},{"$ref":"#/components/schemas/McpReinviteUserParams"},{"$ref":"#/components/schemas/McpForceResetUserParams"},{"$ref":"#/components/schemas/McpGetUserActivityParams"},{"$ref":"#/components/schemas/McpListRolesParams"},{"$ref":"#/components/schemas/McpGetRoleParams"},{"$ref":"#/components/schemas/McpCreateRoleParams"},{"$ref":"#/components/schemas/McpUpdateRoleParams"},{"$ref":"#/components/schemas/McpDisableRoleParams"},{"$ref":"#/components/schemas/McpAddRolePolicyParams"},{"$ref":"#/components/schemas/McpRemoveRolePolicyParams"},{"$ref":"#/components/schemas/McpGetRoleUsersParams"},{"$ref":"#/components/schemas/McpGetRoleGroupsParams"},{"$ref":"#/components/schemas/McpListFeaturesParams"},{"$ref":"#/components/schemas/McpCreateFeatureParams"},{"$ref":"#/components/schemas/McpUpdateFeatureParams"},{"$ref":"#/components/schemas/McpDeprecateFeatureParams"},{"$ref":"#/components/schemas/McpListUserGroupsParams"},{"$ref":"#/components/schemas/McpGetUserGroupParams"},{"$ref":"#/components/schemas/McpCreateUserGroupParams"},{"$ref":"#/components/schemas/McpUpdateUserGroupParams"},{"$ref":"#/components/schemas/McpDeleteUserGroupParams"},{"$ref":"#/components/schemas/McpUpdateGroupMembersParams"},{"$ref":"#/components/schemas/McpUpdateGroupRolesParams"},{"$ref":"#/components/schemas/McpListRegionsParams"},{"$ref":"#/components/schemas/McpCreateRegionParams"},{"$ref":"#/components/schemas/McpUpdateRegionParams"},{"$ref":"#/components/schemas/McpDeleteRegionParams"},{"$ref":"#/components/schemas/McpListDepartmentsParams"},{"$ref":"#/components/schemas/McpCreateDepartmentParams"},{"$ref":"#/components/schemas/McpUpdateDepartmentParams"},{"$ref":"#/components/schemas/McpDeleteDepartmentParams"},{"$ref":"#/components/schemas/McpListTagsParams"},{"$ref":"#/components/schemas/McpCreateTagParams"},{"$ref":"#/components/schemas/McpUpdateTagParams"},{"$ref":"#/components/schemas/McpDeleteTagParams"},{"$ref":"#/components/schemas/McpListClientPipelineTasksParams"},{"$ref":"#/components/schemas/McpStopClientPipelineTaskParams"},{"$ref":"#/components/schemas/McpListKnowledgeGapsParams"},{"$ref":"#/components/schemas/McpRunKnowledgeGapDetectionParams"},{"$ref":"#/components/schemas/McpDiagnosticStartParams"},{"$ref":"#/components/schemas/McpDiagnosticGetSessionParams"},{"$ref":"#/components/schemas/McpDiagnosticChatParams"},{"$ref":"#/components/schemas/McpSearchHubspotCompaniesParams"},{"$ref":"#/components/schemas/McpGetHubspotCompanyMappingParams"},{"$ref":"#/components/schemas/McpSetHubspotCompanyMappingParams"},{"$ref":"#/components/schemas/McpRemoveHubspotCompanyMappingParams"},{"$ref":"#/components/schemas/McpPushHubspotChatVolumeParams"},{"$ref":"#/components/schemas/McpCreateHubspotCompanyParams"}]}}}}}}}},"responses":{"200":{"description":"JSON-RPC response or SSE stream","content":{"application/json":{"schema":{"type":"object"}},"text/event-stream":{"schema":{"type":"string"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"get":{"tags":["MCP"],"operationId":"mcpSse","summary":"MCP SSE stream","description":"Server-Sent Events stream for MCP server notifications. Requires established session.","security":[{"apiKey":[]}],"parameters":[{"name":"mcp-session-id","in":"header","required":true,"schema":{"type":"string"},"description":"Established session ID"}],"responses":{"200":{"description":"SSE stream","content":{"text/event-stream":{"schema":{"type":"string"}}}},"404":{"description":"Session not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["MCP"],"operationId":"mcpClose","summary":"Close MCP session","description":"End an MCP session and release resources.","security":[{"apiKey":[]}],"parameters":[{"name":"mcp-session-id","in":"header","required":true,"schema":{"type":"string"},"description":"Session ID to close"}],"responses":{"200":{"description":"Session closed"},"404":{"description":"Session not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/guest/mcp":{"post":{"tags":["Guest MCP"],"operationId":"mcpGuestPost","summary":"Guest MCP request handler","description":"Streamable HTTP transport for the guest MCP surface. Handles initialize, tools/list, tools/call, resources/list, resources/read, and other JSON-RPC 2.0 messages. Three tools are exposed: `search_hotels` returns the Maison hotels an assistant may talk to, optionally filtered by a free-text query; `get_hotel_info` returns one hotel's card — name, website, address, and imagery; `ask_hotel` puts a guest question to that hotel's concierge and returns its answer. Two tiers of access: **Tier 1** — browsing and asking — needs no credentials and is what this mount serves. **Tier 2** — anything that changes a booking or reveals a guest's own reservation — is not exposed as a tool: the concierge steps the guest up inside the chat funnel with a one-time code sent to the email on the booking, so no Maison login ever passes through the assistant. Tool results arrive on three channels: `content` (a short sentence), `structuredContent` (the scalars the model reads verbatim), and `_meta` (delivered to the widget and hidden from the model, which is why the imagery costs no model context) — see McpGuestToolResult. The mount also serves MCP resources: `resources/list` and `resources/read` return two `ui://maison/guest/…` MCP-Apps widget templates, which a host that supports them renders in place of raw JSON — one behind `search_hotels`, one behind `get_hotel_info`. `ask_hotel` has no widget; its result is the hotel's prose answer. `tools/list` publishes an `outputSchema` for the two widget tools — the declared shape of their `structuredContent`, and what a host hydrates a template from — while `ask_hotel` declares none, matching the fact that it sends no `structuredContent` at all. All three tools carry MCP `annotations`, because a tool that declares none defaults to destructive and open-world: `search_hotels` and `get_hotel_info` are read-only, idempotent lookups over the server-side directory of enabled hotels (`readOnlyHint: true`, `idempotentHint: true`, `openWorldHint: false`), while `ask_hotel` is `readOnlyHint: false` and `idempotentHint: false` — a turn can create a reservation and mint a live pay-link, and the same message sent twice can book twice. None of the three is destructive (`destructiveHint: false`): `ask_hotel` only ever adds, and calling an additive tool destructive trains a host to dismiss a warning that should mean something. A client that ignores resources entirely still works: `content` and `structuredContent` carry everything needed to continue, including every addressable `clientId`. Include the `mcp-session-id` header for established sessions.","parameters":[{"name":"mcp-session-id","in":"header","schema":{"type":"string"},"description":"Session ID returned from initialize. Omit for the initial request."}],"requestBody":{"required":true,"description":"JSON-RPC 2.0 message. For `tools/call`, the `params.arguments` object conforms to one of the guest tool schemas: McpGuestSearchHotelsParams, McpGuestGetHotelInfoParams, McpGuestAskHotelParams. Argument names are snake_case, exactly as the tools register them. The operator `Mcp*Params` schemas are NOT accepted here.","content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","id","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"oneOf":[{"type":"string"},{"type":"number"}]},"method":{"type":"string","description":"MCP method — initialize, tools/list, tools/call, resources/list, resources/read, and the standard notifications. The two resources methods serve the MCP-Apps widget templates; a client that never calls them still gets a complete, usable result from every tool."},"params":{"type":"object","properties":{"name":{"type":"string","description":"Tool name (for tools/call)","enum":["search_hotels","get_hotel_info","ask_hotel"]},"arguments":{"oneOf":[{"$ref":"#/components/schemas/McpGuestSearchHotelsParams"},{"$ref":"#/components/schemas/McpGuestGetHotelInfoParams"},{"$ref":"#/components/schemas/McpGuestAskHotelParams"}]}}}}}}}},"responses":{"200":{"description":"JSON-RPC response, or an SSE stream when the client accepts one. The `result` follows the method: `tools/call` returns a McpGuestToolResult; `resources/read` returns `{ contents: [McpGuestWidgetTemplate] }`; `resources/list` returns the same two templates as metadata; `initialize` and `tools/list` return the standard MCP payloads. A tool that refuses still answers 200 — the refusal is `isError: true` inside the result, not an HTTP status.","content":{"application/json":{"schema":{"type":"object"}},"text/event-stream":{"schema":{"type":"string"}}}},"400":{"description":"Malformed JSON-RPC message — unparseable, an oversized batch, an oversized string request id, or a first request that is not `initialize`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Session not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"415":{"description":"Unsupported media type — the request body must be sent as `application/json`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Rate limited — either the mount is opening sessions faster than its ceiling allows, or this session has exhausted its own request budget. Retry after a short pause.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"At capacity — the guest mount is holding its maximum number of concurrent sessions. Retry later.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"get":{"tags":["Guest MCP"],"operationId":"mcpGuestSse","summary":"Guest MCP SSE stream","description":"Server-Sent Events stream for guest MCP server notifications. Requires an established session — POST an initialize message first and reuse the session ID it returns.","parameters":[{"name":"mcp-session-id","in":"header","required":true,"schema":{"type":"string"},"description":"Established session ID"}],"responses":{"200":{"description":"SSE stream","content":{"text/event-stream":{"schema":{"type":"string"}}}},"404":{"description":"Session not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Rate limited — this session has exhausted its own request budget. Retry after a short pause.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Guest MCP"],"operationId":"mcpGuestClose","summary":"Close guest MCP session","description":"End a guest MCP session and release its resources.","parameters":[{"name":"mcp-session-id","in":"header","required":true,"schema":{"type":"string"},"description":"Session ID to close"}],"responses":{"200":{"description":"Session closed"},"404":{"description":"Session not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients":{"get":{"tags":["Clients"],"operationId":"listClients","summary":"List clients","description":"Return a paginated, searchable, sortable list of active clients. Requires session auth and clients:READ permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of clients to return (1-100)"},{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response as `nextCursor`"},{"name":"search","in":"query","schema":{"type":"string","maxLength":100},"description":"Case-insensitive search string matched against client name, description, and website domain"},{"name":"status","in":"query","schema":{"type":"string","enum":["active","disabled","all"]},"description":"Filter by lifecycle status. `active` (default) excludes disabled clients, `disabled` shows only disabled, `all` returns both."},{"name":"order_by","in":"query","schema":{"type":"string","enum":["name","created_date","website_id","demo_client","last_scraped_date","ai_visibility_schedule"],"default":"created_date"},"description":"Column to sort by"},{"name":"sort","in":"query","schema":{"type":"string","enum":["ASC","DESC"],"default":"DESC"},"description":"Sort direction"}],"responses":{"200":{"description":"Paginated list of clients","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientListResponse"}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Clients"],"operationId":"createClient","summary":"Create client","description":"Create a new client record. Requires session auth and clients:WRITE permission.","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateClientBody"}}}},"responses":{"201":{"description":"Client created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/summary":{"get":{"tags":["Clients"],"operationId":"getClientsSummary","summary":"Get clients summary","description":"Return aggregate counts across all active clients. Requires session auth and clients:READ permission.","security":[{"sessionAuth":[]}],"responses":{"200":{"description":"Aggregate client counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientsSummaryResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/metrics":{"get":{"tags":["Clients"],"operationId":"getClientMetrics","summary":"Get client Overview metrics","description":"Returns metrics used by the Client Overview tab. Today this is a stub that returns an empty metrics object; shape will stay stable while fields are added in later features.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Client metrics (empty for now)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientMetricsResponse"}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/website/rescrape":{"post":{"tags":["Clients"],"operationId":"rescrapeClientWebsite","summary":"Trigger a fresh website scrape","description":"Schedules a fresh scrape of the client's primary website. Returns immediately; the response indicates whether the task was accepted for processing (enqueued=true) or recorded but not yet dispatched (enqueued=false, typically in non-production environments).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescrapeClientBody"}}}},"responses":{"202":{"description":"Scrape session recorded and (optionally) enqueued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescrapeClientResponse"}}}},"400":{"description":"Invalid client id or body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Client has no website to rescrape or is disabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/demosite":{"post":{"tags":["Clients"],"operationId":"createClientDemoSite","summary":"Create or recreate the demo preview site","description":"Generates or refreshes a preview demo site for the client that embeds the client's website and the Maison chat widget. Returns the public URL of the created site. Returns 503 when the preview-site feature is not configured in this environment.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DemoSiteBody"}}}},"responses":{"201":{"description":"Demo site created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DemoSiteResponse"}}}},"400":{"description":"Invalid client id or body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Preview-site not configured in this environment","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}":{"get":{"tags":["Clients"],"operationId":"getClient","summary":"Get client by ID","description":"Fetch a single client record by UUID. Requires session auth and clients:READ permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Client record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Clients"],"operationId":"updateClient","summary":"Update client","description":"Partially update a client record. All body fields are optional. Requires session auth and clients:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateClientBody"}}}},"responses":{"200":{"description":"Updated client record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Client is disabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Clients"],"operationId":"deleteClient","summary":"Disable client","description":"Soft-disable a client by setting disabled_date and disabled_by. The record is retained for audit purposes. Requires session auth and clients:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Client disabled","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Client is disabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/enable":{"post":{"tags":["Clients"],"operationId":"enableClient","summary":"Re-enable a soft-disabled client","description":"Clears `disabled_date` and `disabled_by` so the client becomes active again. Requires clients:WRITE.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Client enabled","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Client is already active","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/features":{"get":{"tags":["Clients"],"operationId":"listClientFeatures","summary":"List client feature entitlements","description":"Return the feature names a client is currently entitled to (layer-1 entitlement, Maison-admin/business-plan tier). Includes omnichannel entitlements such as `channels` (the master switch for the Channels admin surface + inbound webhook auto-replies). Used by the Business Console Channels tab to render the master toggle. Requires global clients:READ (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Entitled feature list","content":{"application/json":{"schema":{"type":"object","required":["features"],"properties":{"features":{"type":"array","items":{"type":"string"},"description":"Feature names the client is entitled to"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Clients"],"operationId":"grantClientFeature","summary":"Grant a feature entitlement to a client","description":"Grants a feature entitlement to a client (Maison-admin/sales business-plan operation). Idempotent: re-granting a previously revoked feature restores it. Granting `channels` unlocks the omnichannel Channels admin surface and enables auto-replies for inbound messages (Instagram/Messenger/WhatsApp). Requires global clients:WRITE (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["feature"],"properties":{"feature":{"type":"string","minLength":1,"maxLength":200,"description":"Feature name to grant (must exist in the features catalog)","example":"channels"}}}}}},"responses":{"201":{"description":"Feature granted","content":{"application/json":{"schema":{"type":"object","required":["feature"],"properties":{"feature":{"type":"string","description":"Granted feature name"}}}}}},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Feature not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/features/{feature}":{"delete":{"tags":["Clients"],"operationId":"revokeClientFeature","summary":"Revoke a feature entitlement from a client","description":"Revokes a feature entitlement from a client. No-op if the client is not entitled to the feature. Revoking `channels` stops inbound webhook auto-replies for the client; existing channel connections are preserved. Requires global clients:WRITE (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"feature","in":"path","required":true,"schema":{"type":"string"},"description":"Feature name to revoke","example":"channels"}],"responses":{"200":{"description":"Feature revoked","content":{"application/json":{"schema":{"type":"object","required":["feature"],"properties":{"feature":{"type":"string","description":"Revoked feature name"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Feature not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/pipelinetasks":{"get":{"tags":["Clients"],"operationId":"listClientPipelineTasks","summary":"List running pipeline tasks for a client","description":"Returns all pipeline tasks currently in RUNNING state for the given client. Use to check whether a scrape or other task is already in progress before triggering a duplicate. Requires MaisonAdmin (ALL EXECUTE) — not accessible to hotel-level client admins.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"List of currently running pipeline tasks","content":{"application/json":{"schema":{"type":"object","required":["tasks","count"],"properties":{"tasks":{"type":"array","items":{"type":"object","required":["taskId","taskType","sourceKind","source","sessionId","startedAt","runningSeconds"],"properties":{"taskId":{"type":"string","description":"Pipeline task identifier"},"taskType":{"type":"string","description":"Pipeline task type (e.g. scrape-website, deduplicate-kb)"},"sourceKind":{"type":"string","enum":["website","document","knowledge","tests","analytics","unknown"],"description":"Semantic category of the data source the task operates on, derived from taskType: scrape-website/sync-website/discover-booking-links → website; scrape-document/ingest-source → document; deduplicate-kb/check-consistency → knowledge; run-prompt-tests → tests; run-ai-visibility → analytics; else → unknown."},"source":{"type":"string","nullable":true,"description":"The URL the task is operating on, or null when not applicable (e.g. for knowledge or document tasks)"},"sessionId":{"type":"string","nullable":true,"description":"Scrape session UUID (null when absent from command)"},"startedAt":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when the task started (falls back to createdAt)"},"runningSeconds":{"type":"integer","description":"Seconds elapsed since the task started"}}}},"count":{"type":"integer","description":"Number of running tasks for this client"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/pipelinetasks/{taskId}":{"delete":{"tags":["Clients"],"operationId":"stopClientPipelineTask","summary":"Stop a running pipeline task for a client","description":"Stops a running pipeline task for the given client. The task must belong to the given client — tasks belonging to other clients are returned as not found (opaque 404). Tasks that have already stopped also return 404. Requires MaisonAdmin (ALL EXECUTE) — not accessible to hotel-level client admins. When a scrape task is stopped, its associated session record is marked as canceled; the `sessionMarked` response field indicates whether this update occurred.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"taskId","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-f]{32}$"},"description":"Pipeline task identifier (32 lowercase hexadecimal characters)"}],"responses":{"200":{"description":"Task stopped successfully","content":{"application/json":{"schema":{"type":"object","required":["stopped","taskId","sessionMarked"],"properties":{"stopped":{"type":"boolean","description":"Always true on success"},"taskId":{"type":"string","description":"The task identifier that was stopped"},"sessionMarked":{"type":"boolean","description":"true when the scrape session associated with this task was marked as canceled. false when the task had no associated session (e.g. non-scrape tasks), the session was already in a terminal status, or the status update could not be applied (best-effort)."}}}}}},"400":{"description":"Invalid client id or task id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Pipeline task not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/channels":{"get":{"tags":["Clients"],"operationId":"listClientChannels","summary":"List client channel connections","description":"Returns the client's active messaging channel connections (never includes access tokens) plus two entitlement booleans: `entitled` = the 'channels' feature (messaging channels), `guestappEntitled` = the 'guestapp' feature (the ChatGPT guest app — gates POST /api/chatgpt/connect). Requires session or API-key auth with update-client READ on the client.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Channel list and entitlement status","content":{"application/json":{"schema":{"type":"object","required":["entitled","guestappEntitled","channels"],"properties":{"entitled":{"type":"boolean","description":"Whether the client is entitled to the Channels feature (client_feature_link row for 'channels' exists and is active)"},"guestappEntitled":{"type":"boolean","description":"Whether the client is entitled to the guestapp feature — the ChatGPT guest app (client_feature_link row for 'guestapp' exists and is active). Gates POST /api/chatgpt/connect."},"channels":{"type":"array","items":{"type":"object","required":["id","channel","channelAccountId","status","autoReplyEnabled","draftOnly","createdDate"],"properties":{"id":{"type":"string","format":"uuid"},"channel":{"type":"string","enum":["INSTAGRAM","MESSENGER","WHATSAPP","GMAIL","OUTLOOK","CHATGPT"],"description":"Channel type"},"channelAccountId":{"type":"string","description":"External account identifier (IG Business Account ID, Messenger Page ID, WhatsApp Phone Number ID; for CHATGPT this is the clientId itself — there is no external account)"},"channelUsername":{"type":"string","nullable":true,"description":"Human-readable account name (username, Page name) — null when not available"},"status":{"type":"string","description":"Connection health status (CONNECTED, ACTION_NEEDED, ERROR)"},"autoReplyEnabled":{"type":"boolean","description":"Whether the AI auto-reply is enabled for this channel"},"draftOnly":{"type":"boolean","description":"Draft mode: when true (GMAIL/OUTLOOK only) Maison stages each reply as a mailbox draft for a human to send instead of sending it. Ignored on other channels. With autoReplyEnabled the reply mode is: off (autoReplyEnabled=false), draft (draftOnly=true), auto-send (otherwise)."},"tokenExpiresAt":{"type":"string","format":"date-time","nullable":true,"description":"Access token expiry (ISO 8601). Null for non-expiring tokens (e.g. Messenger page tokens)"},"createdDate":{"type":"string","format":"date-time","description":"When this channel connection was first established"}}}}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to list channel connections","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/channels/{channelId}":{"delete":{"tags":["Clients"],"operationId":"disconnectChannel","summary":"Disconnect a channel","description":"Soft-deletes the channel connection row (sets deleted_date). Does not revoke the token from Meta. Then, best effort, clears the guest routes for the account and purges the sessions the connection carried from the owner's session partition and from every routed child partition; sessions stamped with any earlier connection id of the same account on this client (superseded reconnects) are purged too. A failure there is logged and traced and the disconnect still succeeds. Requires update-client WRITE on the client.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"channelId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Channel connection UUID"}],"responses":{"204":{"description":"Channel disconnected"},"400":{"description":"Invalid id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Channel connection not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to disconnect channel","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"patch":{"tags":["Clients"],"operationId":"toggleChannelAutoReply","summary":"Update a channel connection's reply settings","description":"Update the reply settings behind a connection's Off / Draft / Auto-send mode. Partial: an omitted field is left unchanged, so turning auto-reply off preserves draftOnly and re-enabling returns to the previous mode. draftOnly is accepted on GMAIL and OUTLOOK connections only (400 otherwise). Requires update-client WRITE on the client.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"channelId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Channel connection UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"At least one property must be present.","minProperties":1,"properties":{"autoReplyEnabled":{"type":"boolean","description":"Whether Maison replies on this connection at all (false = Off)."},"draftOnly":{"type":"boolean","description":"Draft mode (GMAIL/OUTLOOK only): stage replies as mailbox drafts for a human to send; side-effecting actions are not taken."}}}}}},"responses":{"200":{"description":"The connection's reply settings after the update","content":{"application/json":{"schema":{"type":"object","required":["autoReplyEnabled","draftOnly"],"properties":{"autoReplyEnabled":{"type":"boolean"},"draftOnly":{"type":"boolean"}}}}}},"400":{"description":"Invalid id or body, or draftOnly requested on a non-email channel","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Channel connection not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to update reply settings","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/channels/instructions":{"get":{"tags":["Clients"],"operationId":"listChannelInstructions","summary":"List all channel instructions","description":"Returns operator style instructions for all channel types keyed by channel TYPE (WEB, GMAIL, OUTLOOK, INSTAGRAM, MESSENGER, WHATSAPP, SMS, CHATGPT uppercase). Requires update-client READ on the client.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Instructions map keyed by channel type","content":{"application/json":{"schema":{"type":"object","required":["instructions"],"properties":{"instructions":{"type":"object","description":"Map of channel type (WEB, GMAIL, OUTLOOK, INSTAGRAM, MESSENGER, WHATSAPP, SMS, CHATGPT) to array of instruction notes","additionalProperties":{"type":"array","items":{"type":"object","required":["id","title","text"],"properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"text":{"type":"string"}}}}}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to list channel instructions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/channels/{channelType}/instructions":{"post":{"tags":["Clients"],"operationId":"createChannelInstruction","summary":"Create a channel instruction","description":"Appends a titled instruction note to the ordered list for the given channel type. Channel type is case-insensitive in the URL (WEB, GMAIL, OUTLOOK, INSTAGRAM, MESSENGER, WHATSAPP, SMS, CHATGPT). Requires update-client WRITE on the client.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"channelType","in":"path","required":true,"schema":{"type":"string","enum":["WEB","GMAIL","OUTLOOK","INSTAGRAM","MESSENGER","WHATSAPP","SMS","CHATGPT"]},"description":"Channel type (case-insensitive in URL)"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","text"],"properties":{"title":{"type":"string","maxLength":100,"description":"Instruction title","example":"Tone for Instagram"},"text":{"type":"string","maxLength":1000,"description":"Instruction body text","example":"Always respond in a warm, concise style. Keep replies under 3 sentences."}}},"example":{"title":"Tone for Instagram","text":"Always respond in a warm, concise style. Keep replies under 3 sentences."}}}},"responses":{"201":{"description":"Created instruction note","content":{"application/json":{"schema":{"type":"object","required":["id","title","text"],"properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"text":{"type":"string"}}}}}},"400":{"description":"Invalid client id, channel type, or body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to create channel instruction","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/channels/{channelType}/instructions/{instructionId}":{"put":{"tags":["Clients"],"operationId":"updateChannelInstruction","summary":"Update a channel instruction","description":"Replaces the title and text of an existing instruction note. Requires update-client WRITE on the client.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"channelType","in":"path","required":true,"schema":{"type":"string","enum":["WEB","GMAIL","OUTLOOK","INSTAGRAM","MESSENGER","WHATSAPP","SMS","CHATGPT"]},"description":"Channel type"},{"name":"instructionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Instruction note UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","text"],"properties":{"title":{"type":"string","maxLength":100,"description":"New title","example":"Tone for Instagram"},"text":{"type":"string","maxLength":1000,"description":"New body text","example":"Always respond in a warm, concise style. Keep replies under 3 sentences."}}},"example":{"title":"Tone for Instagram","text":"Always respond in a warm, concise style. Keep replies under 3 sentences."}}}},"responses":{"200":{"description":"Updated instruction note","content":{"application/json":{"schema":{"type":"object","required":["id","title","text"],"properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"text":{"type":"string"}}}}}},"400":{"description":"Invalid client id, channel type, instruction id, or body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Instruction not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to update channel instruction","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Clients"],"operationId":"deleteChannelInstruction","summary":"Delete a channel instruction","description":"Removes a titled instruction note from the channel's ordered list. Requires update-client WRITE on the client.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"channelType","in":"path","required":true,"schema":{"type":"string","enum":["WEB","GMAIL","OUTLOOK","INSTAGRAM","MESSENGER","WHATSAPP","SMS","CHATGPT"]},"description":"Channel type"},{"name":"instructionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Instruction note UUID"}],"responses":{"204":{"description":"Instruction deleted"},"400":{"description":"Invalid client id, channel type, or instruction id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to delete channel instruction","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/instagram/connect":{"get":{"tags":["Clients"],"operationId":"instagramConnectInitiate","summary":"Start Instagram Business Login OAuth flow","description":"Initiates the Instagram Business Login flow for a client. Requires session or API-key auth with update-client WRITE on the client. Redirects (302) to the Instagram authorize URL with an HMAC-signed state binding clientId + userId. Optionally accepts a `returnTo` query param (allowlisted to /console/ and /qa/ prefixes) — if provided, the callback will redirect to that URL instead of the default Channels tab.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"clientId","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID to connect an Instagram account for"},{"name":"returnTo","in":"query","required":false,"schema":{"type":"string"},"description":"URL to redirect to after OAuth completion. Must start with {siteHost}/console/ or {siteHost}/qa/. The result param (instagram=connected|denied|error) is appended. Rejected with 400 if not allowlisted."}],"responses":{"302":{"description":"Redirect to Instagram authorize URL"},"400":{"description":"Invalid clientId or returnTo not allowlisted","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Instagram connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/instagram/connect/callback":{"get":{"tags":["Clients"],"operationId":"instagramConnectCallback","summary":"Instagram Business Login OAuth callback","description":"Meta redirects here after the user completes or denies the Instagram authorization flow. Verifies the HMAC-signed state, exchanges the code for a long-lived token, upserts the channel connection, and redirects back to the Channels tab (or the returnTo URL from state if present and allowlisted).","parameters":[{"name":"code","in":"query","schema":{"type":"string"},"description":"Authorization code from Meta"},{"name":"state","in":"query","schema":{"type":"string"},"description":"HMAC-signed state from the initiate step"},{"name":"error","in":"query","schema":{"type":"string"},"description":"Error code when the user denied the flow"}],"responses":{"302":{"description":"Redirect to Channels tab or returnTo URL"},"400":{"description":"Missing code/state or invalid state","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Instagram connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/messenger/connect":{"get":{"tags":["Clients"],"operationId":"messengerConnectInitiate","summary":"Start Messenger Facebook Login for Business OAuth flow","description":"Initiates the Messenger connect flow (Facebook Login for Business) for a client. Requires session or API-key auth with update-client WRITE. Redirects (302) to the Facebook authorize URL with an HMAC-signed state. Optionally accepts a `returnTo` query param (allowlisted to /console/ and /qa/ prefixes).","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"clientId","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID to connect a Messenger Page for"},{"name":"returnTo","in":"query","required":false,"schema":{"type":"string"},"description":"URL to redirect to after OAuth completion. Must start with {siteHost}/console/ or {siteHost}/qa/. The result param (messenger=connected|denied|error|nopages|multiplepages) is appended. Rejected with 400 if not allowlisted."}],"responses":{"302":{"description":"Redirect to Facebook authorize URL"},"400":{"description":"Invalid clientId or returnTo not allowlisted","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Messenger connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/messenger/connect/callback":{"get":{"tags":["Clients"],"operationId":"messengerConnectCallback","summary":"Messenger OAuth callback","description":"Facebook redirects here after the user completes or denies the Messenger authorization flow. Verifies state, exchanges the code for a user token, retrieves managed Pages, subscribes the single Page to the app, upserts the channel connection, and redirects back to Channels tab (or returnTo if in state and allowlisted). Returns nopages or multiplepages result when the user's token covers 0 or >1 Pages.","parameters":[{"name":"code","in":"query","schema":{"type":"string"},"description":"Authorization code from Facebook"},{"name":"state","in":"query","schema":{"type":"string"},"description":"HMAC-signed state from the initiate step"},{"name":"error","in":"query","schema":{"type":"string"},"description":"Error code when the user denied the flow"}],"responses":{"302":{"description":"Redirect to Channels tab or returnTo URL"},"400":{"description":"Missing code/state or invalid state","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Messenger connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/whatsapp/connect":{"get":{"tags":["Clients"],"operationId":"whatsappConnectConfig","summary":"Get WhatsApp Embedded Signup config","description":"Returns the public Embedded Signup config { appId, configId, state } for the Facebook JS SDK popup flow. Unlike Instagram/Messenger (redirect OAuth), WhatsApp connects client-side via FB.login with a config_id; the HMAC-signed state binds {clientId, userId} for CSRF verification at the POST callback. Requires session or API-key auth with update-client WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"clientId","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID to connect a WhatsApp Business number for"}],"responses":{"200":{"description":"Embedded Signup config","content":{"application/json":{"schema":{"type":"object","properties":{"appId":{"type":"string"},"configId":{"type":"string"},"state":{"type":"string"}}}}}},"400":{"description":"Invalid clientId","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"WhatsApp connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/whatsapp/connect/callback":{"post":{"tags":["Clients"],"operationId":"whatsappConnectCallback","summary":"Complete WhatsApp Embedded Signup","description":"Submits the Embedded Signup result. Verifies the HMAC-signed state (clientId + userId binding), exchanges the code for a business token, verifies WABA ownership, registers the phone number, subscribes the WABA to the app, and upserts the channel connection. phoneNumberId is omitted on the Coexistence path (FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING carries only the waba_id) — the number is then resolved from the WABA and registration is skipped. Requires session or API-key auth with update-client WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["clientId","code","wabaId","state"],"properties":{"clientId":{"type":"string","format":"uuid"},"code":{"type":"string"},"wabaId":{"type":"string"},"phoneNumberId":{"type":"string"},"state":{"type":"string"}}}}}},"responses":{"200":{"description":"WhatsApp Business connected"},"400":{"description":"Invalid body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Invalid or mismatched state","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"Meta API error during code exchange or WABA setup","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"WhatsApp connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/outlook/connect":{"get":{"tags":["Clients"],"operationId":"outlookConnectInitiate","summary":"Start Outlook (Microsoft Graph) OAuth flow","description":"Initiates the delegated Microsoft Graph OAuth flow for a client. Requires session or API-key auth with update-client WRITE on the client. Redirects (302) to the Microsoft authorize URL with an HMAC-signed state binding clientId + userId (signed with the Microsoft client secret). Optionally accepts a `returnTo` query param (allowlisted to /console/ and /qa/ prefixes) — if provided, the callback will redirect to that URL instead of the default Channels tab.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"clientId","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID to connect an Outlook mailbox for"},{"name":"returnTo","in":"query","required":false,"schema":{"type":"string"},"description":"URL to redirect to after OAuth completion. Must start with {siteHost}/console/ or {siteHost}/qa/. The result param (outlook=connected|denied|error) is appended. Rejected with 400 if not allowlisted."}],"responses":{"302":{"description":"Redirect to Microsoft authorize URL"},"400":{"description":"Invalid clientId or returnTo not allowlisted","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Outlook connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/outlook/connect/callback":{"get":{"tags":["Clients"],"operationId":"outlookConnectCallback","summary":"Outlook (Microsoft Graph) OAuth callback","description":"Microsoft redirects here after the user completes or denies the Outlook authorization flow. Verifies the HMAC-signed state, exchanges the code for access + refresh tokens, reads the mailbox address from Graph /me, upserts the OUTLOOK channel connection, and redirects back to the Channels tab (or the returnTo URL from state if present and allowlisted).","parameters":[{"name":"code","in":"query","schema":{"type":"string"},"description":"Authorization code from Microsoft"},{"name":"state","in":"query","schema":{"type":"string"},"description":"HMAC-signed state from the initiate step"},{"name":"error","in":"query","schema":{"type":"string"},"description":"Error code when the user denied the flow"}],"responses":{"302":{"description":"Redirect to Channels tab or returnTo URL"},"400":{"description":"Missing code/state or invalid state","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Outlook connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/gmail/connect":{"get":{"tags":["Clients"],"operationId":"gmailConnectInitiate","summary":"Start Gmail (Google) OAuth flow","description":"Initiates the Gmail send-first OAuth flow for a client. Requires session or API-key auth with update-client WRITE on the client. Redirects (302) to the Google authorize URL (access_type=offline, prompt=consent so a refresh token is returned) with an HMAC-signed state binding clientId + userId (signed with the Google client secret). Optionally accepts a `returnTo` query param (allowlisted to /console/ and /qa/ prefixes) — if provided, the callback will redirect to that URL instead of the default Channels tab.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"clientId","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID to connect a Gmail mailbox for"},{"name":"returnTo","in":"query","required":false,"schema":{"type":"string"},"description":"URL to redirect to after OAuth completion. Must start with {siteHost}/console/ or {siteHost}/qa/. The result param (gmail=connected|denied|error) is appended. Rejected with 400 if not allowlisted."}],"responses":{"302":{"description":"Redirect to Google authorize URL"},"400":{"description":"Invalid clientId or returnTo not allowlisted","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Gmail connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/gmail/connect/callback":{"get":{"tags":["Clients"],"operationId":"gmailConnectCallback","summary":"Gmail (Google) OAuth callback","description":"Google redirects here after the user completes or denies the Gmail authorization flow. Verifies the HMAC-signed state, exchanges the code for access + refresh tokens, reads the mailbox address from the id_token email claim, upserts the GMAIL channel connection, and redirects back to the Channels tab (or the returnTo URL from state if present and allowlisted).","parameters":[{"name":"code","in":"query","schema":{"type":"string"},"description":"Authorization code from Google"},{"name":"state","in":"query","schema":{"type":"string"},"description":"HMAC-signed state from the initiate step"},{"name":"error","in":"query","schema":{"type":"string"},"description":"Error code when the user denied the flow"}],"responses":{"302":{"description":"Redirect to Channels tab or returnTo URL"},"400":{"description":"Missing code/state or invalid state","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Gmail connect is not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/chatgpt/connect":{"post":{"tags":["Clients"],"operationId":"chatgptConnect","summary":"Connect the ChatGPT guest app channel","description":"Opts a client's hotel into the ChatGPT guest app (the /guest/mcp surface). Unlike the Meta/email connects there is no external OAuth — there is nothing to hand-shake with on OpenAI's side — so this is a plain JSON POST with no callback: it records an active CHATGPT row in client_channels (status CONNECTED, no token). That row is one of the two SQL keys the guest MCP hotel directory intersects; the other is the Maison-admin 'guestapp' feature entitlement, which this endpoint requires up front so a connect can only narrow within what the entitlement grants, never widen it. Idempotent: connecting an already-connected client writes nothing and returns alreadyConnected: true. Disconnect reuses the generic DELETE /api/clients/{id}/channels/{channelId}. Requires session or API-key auth with update-client WRITE on the client. Returns 503 in environments where the guest MCP surface is disabled (GUEST_MCP_ENABLED).","security":[{"sessionAuth":[]},{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["clientId"],"properties":{"clientId":{"type":"string","format":"uuid","description":"Client UUID to opt into the ChatGPT guest app"}}}}}},"responses":{"200":{"description":"ChatGPT guest app connected (or already connected)","content":{"application/json":{"schema":{"type":"object","required":["connected","alreadyConnected"],"properties":{"connected":{"type":"boolean"},"alreadyConnected":{"type":"boolean","description":"true when an active CHATGPT connection already existed and nothing was written"}}}}}},"400":{"description":"Invalid or missing clientId","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden — the principal lacks update-client WRITE on the client, or the client is not entitled to the 'guestapp' feature","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"ChatGPT connect is not enabled in this environment","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/users":{"get":{"tags":["Users"],"operationId":"listUsers","summary":"List users","description":"Return a paginated, searchable, sortable list of users. Requires session auth and users:READ permission.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of users to return (1-100)"},{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response as `nextCursor`"},{"name":"search","in":"query","schema":{"type":"string","maxLength":100},"description":"Case-insensitive search string matched against name and email"},{"name":"status","in":"query","schema":{"type":"string","enum":["active","disabled","invited","expired"]},"description":"Filter by computed user status"},{"name":"order_by","in":"query","schema":{"type":"string","enum":["email","first_name","last_name","created_date","last_login_date","status"],"default":"created_date"},"description":"Column to sort by"},{"name":"sort","in":"query","schema":{"type":"string","enum":["ASC","DESC"],"default":"DESC"},"description":"Sort direction"}],"responses":{"200":{"description":"Paginated list of users","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserListResponse"}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/users/invite":{"post":{"tags":["Users"],"operationId":"inviteUser","summary":"Invite a new user","description":"Create a user record and send an invitation email. The user must accept the invite to activate their account. Requires session auth and users:WRITE permission.","security":[{"sessionAuth":[]},{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteUserBody"}}}},"responses":{"201":{"description":"User invited — invitation email sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteUserResponse"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"A user with this email already exists","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process invitation","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/users/{id}/reinvite":{"post":{"tags":["Users"],"operationId":"reinviteUser","summary":"Reinvite a user","description":"Resend an invitation to a user whose status is 'invited' or 'expired'. Resets the invite expiry date. Returns 409 if the user has already accepted their invite or is disabled. Requires session auth and users:WRITE permission.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReinviteUserBody"}}}},"responses":{"200":{"description":"Invite resent — updated expiry date returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteUserResponse"}}}},"400":{"description":"Invalid user id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"User has already accepted their invite or is disabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process reinvite","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/users/{id}/forcereset":{"post":{"tags":["Users"],"operationId":"forceResetUser","summary":"Force password reset for a user","description":"Sets force_reset_password = true and sends a password reset email via Firebase. The user must be active (accepted invite, not disabled). Returns 409 if the user has not accepted their invite or is disabled. If email sending fails the flag is still set and 200 is returned. Requires session auth and users:WRITE permission.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"}],"responses":{"200":{"description":"Password reset email sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForceResetResponse"}}}},"400":{"description":"Invalid user id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"User has not accepted their invite yet or is disabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/users/{id}/setpassword":{"post":{"tags":["Users"],"operationId":"setUserPassword","summary":"Directly set a user's password (admin only)","description":"Directly sets the user's Firebase password to the supplied value. Requires session auth and users:WRITE permission. NOTE: This endpoint is intentionally REST-only and is NOT exposed via A2A or MCP. Direct admin password-set is a sensitive privileged operation — force-reset (POST /api/users/{id}/forcereset) is the agent-safe path that sends a password reset email instead.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["password"],"properties":{"password":{"type":"string","minLength":8}}}}}},"responses":{"200":{"description":"Password updated","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"400":{"description":"Invalid user id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/users/{id}/activity":{"get":{"tags":["Users"],"operationId":"getUserActivity","summary":"Get user activity timeline","description":"Return a paginated activity timeline for a user. Events are assembled from role assignments, group membership changes, API key operations, invitations, and login history. Ordered by event_date DESC. Requires session auth and users:READ permission.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Maximum number of events to return (1-100)"},{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response as `nextCursor`"}],"responses":{"200":{"description":"Paginated activity timeline","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserActivityResponse"}}}},"400":{"description":"Invalid user id or query parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/users/{id}":{"get":{"tags":["Users"],"operationId":"getUser","summary":"Get user by ID","description":"Fetch a single user record by UUID. Requires session auth and users:READ permission.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"}],"responses":{"200":{"description":"User record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserListItem"}}}},"400":{"description":"Invalid user id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Users"],"operationId":"updateUser","summary":"Update user","description":"Partially update a user record. All body fields are optional. Set `enable: false` to disable or `enable: true` to re-enable. Requires session auth and users:WRITE permission.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserBody"}}}},"responses":{"200":{"description":"Updated user record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserListItem"}}}},"400":{"description":"Invalid user id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"User is already in the requested state","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Users"],"operationId":"disableUser","summary":"Disable user","description":"Soft-disable a user by setting disabled_date and disabled_by. The record is retained for audit purposes. Requires session auth and users:WRITE permission.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"}],"responses":{"200":{"description":"User disabled","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"400":{"description":"Invalid user id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"User is already disabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/roles":{"get":{"tags":["Roles"],"operationId":"listRoles","summary":"List roles","description":"Return a paginated, searchable, sortable list of roles. Requires session auth and roles:READ permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of roles to return (1-100)"},{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response as `nextCursor`"},{"name":"search","in":"query","schema":{"type":"string","maxLength":100},"description":"Case-insensitive search string matched against role name and description"},{"name":"order_by","in":"query","schema":{"type":"string","enum":["name","created_date","policy_count","user_count"],"default":"created_date"},"description":"Column to sort by"},{"name":"sort","in":"query","schema":{"type":"string","enum":["ASC","DESC"],"default":"DESC"},"description":"Sort direction"}],"responses":{"200":{"description":"Paginated list of roles","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleListResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Roles"],"operationId":"createRole","summary":"Create role","description":"Create a new role. Requires session auth and roles:WRITE permission.","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRoleBody"}}}},"responses":{"201":{"description":"Role created","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/roles/{id}":{"get":{"tags":["Roles"],"operationId":"getRole","summary":"Get role by ID","description":"Fetch a single role with its policies. Requires session auth and roles:READ permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Role UUID"}],"responses":{"200":{"description":"Role detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleDetail"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Role not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Roles"],"operationId":"updateRole","summary":"Update role","description":"Partially update a role. All body fields are optional; at least one must be provided. Requires session auth and roles:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Role UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRoleBody"}}}},"responses":{"200":{"description":"Updated role detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleDetail"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Role not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Roles"],"operationId":"disableRole","summary":"Disable role","description":"Soft-disable a role by setting disabled_date and disabled_by. The record is retained for audit purposes. Requires session auth and roles:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Role UUID"}],"responses":{"200":{"description":"Role disabled","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Role not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/roles/{id}/policies":{"post":{"tags":["Roles"],"operationId":"addPolicy","summary":"Add policy to role","description":"Attach a new policy to a role. Requires session auth and roles:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Role UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddPolicyBody"}}}},"responses":{"201":{"description":"Policy added","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Role not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/roles/{id}/policies/{policyId}":{"delete":{"tags":["Roles"],"operationId":"removePolicy","summary":"Remove policy from role","description":"Remove a policy from a role. Requires session auth and roles:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Role UUID"},{"name":"policyId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Policy UUID"}],"responses":{"200":{"description":"Policy removed","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Role or policy not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/roles/{id}/users/{userId}":{"delete":{"tags":["Roles"],"operationId":"removeRoleUser","summary":"Remove a user's direct role link","description":"Soft-deletes every active direct user_role_link for the given (role, user). Does NOT touch group-inherited access. Idempotent — returns { removed } count.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Role UUID"},{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"}],"responses":{"200":{"description":"Remove operation completed","content":{"application/json":{"schema":{"type":"object","properties":{"removed":{"type":"integer"}}}}}},"400":{"description":"Invalid role or user id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/roles/{id}/users":{"post":{"tags":["Roles"],"operationId":"addRoleUser","summary":"Attach a user to a role","description":"Create a direct user_role_link between this role and the given user. Optional client_id scopes the assignment to a single client; omit for a global link. Reviving a soft-deleted link is idempotent.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Role UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id"],"properties":{"user_id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid","nullable":true,"description":"Optional client scope; null/omitted creates a global link."}}}}}},"responses":{"201":{"description":"User attached to role","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"get":{"tags":["Roles"],"operationId":"listRoleUsers","summary":"List users assigned to a role","description":"Return a paginated list of users assigned to this role. Requires session auth and roles:READ permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Role UUID"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of users to return (1-100)"},{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response as `nextCursor`"}],"responses":{"200":{"description":"Paginated list of users assigned to this role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleUsersResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/roles/{id}/groups":{"post":{"tags":["Roles"],"operationId":"addRoleGroup","summary":"Attach a user group to a role","description":"Create a user_group_role_link between this role and the given user group. Optional client_id scopes the assignment to a single client. Reviving a soft-deleted link is idempotent.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Role UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_group_id"],"properties":{"user_group_id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid","nullable":true,"description":"Optional client scope; null/omitted creates a global link."}}}}}},"responses":{"201":{"description":"Group attached to role","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"get":{"tags":["Roles"],"operationId":"listRoleGroups","summary":"List groups assigned to a role","description":"Return a paginated list of groups assigned to this role. Requires session auth and roles:READ permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Role UUID"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of groups to return (1-100)"},{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response as `nextCursor`"}],"responses":{"200":{"description":"Paginated list of groups assigned to this role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleGroupsResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/features":{"get":{"tags":["Features"],"operationId":"listFeatures","summary":"List features","description":"Return a paginated, searchable list of features. Requires session auth and features:READ permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of features to return (1-100)"},{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response as `nextCursor`"},{"name":"search","in":"query","schema":{"type":"string","maxLength":100},"description":"Case-insensitive search string matched against feature name and description"}],"responses":{"200":{"description":"Paginated list of features","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureListResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Features"],"operationId":"createFeature","summary":"Create feature","description":"Create a new feature. Requires session auth and features:WRITE permission.","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFeatureBody"}}}},"responses":{"201":{"description":"Feature created","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/features/{id}":{"put":{"tags":["Features"],"operationId":"updateFeature","summary":"Update feature","description":"Partially update a feature. All body fields are optional. Requires session auth and features:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Feature ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureBody"}}}},"responses":{"200":{"description":"Updated feature","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureItem"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Feature not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Features"],"operationId":"deprecateFeature","summary":"Deprecate feature","description":"Mark a feature as deprecated. The record is retained. Requires session auth and features:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Feature ID"}],"responses":{"200":{"description":"Feature deprecated","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Feature not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/usergroups":{"get":{"tags":["User Groups"],"operationId":"listUserGroups","summary":"List user groups","description":"Return a paginated, searchable, sortable list of user groups. Requires session auth and user-groups:READ permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of groups to return (1-100)"},{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response as `nextCursor`"},{"name":"search","in":"query","schema":{"type":"string","maxLength":100},"description":"Case-insensitive search string matched against group name and description"},{"name":"order_by","in":"query","schema":{"type":"string","enum":["name","member_count","created_date"],"default":"created_date"},"description":"Column to sort by"},{"name":"sort","in":"query","schema":{"type":"string","enum":["ASC","DESC"],"default":"DESC"},"description":"Sort direction"}],"responses":{"200":{"description":"Paginated list of user groups","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserGroupListResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["User Groups"],"operationId":"createUserGroup","summary":"Create user group","description":"Create a new user group. Requires session auth and user-groups:WRITE permission.","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserGroupBody"}}}},"responses":{"201":{"description":"User group created","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/usergroups/{id}":{"get":{"tags":["User Groups"],"operationId":"getUserGroup","summary":"Get user group detail","description":"Fetch a single user group with its members and roles. Requires session auth and user-groups:READ permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User group UUID"}],"responses":{"200":{"description":"User group detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserGroupDetail"}}}},"400":{"description":"Invalid group id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User group not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["User Groups"],"operationId":"updateUserGroup","summary":"Update user group","description":"Partially update a user group. All body fields are optional. Requires session auth and user-groups:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User group UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserGroupBody"}}}},"responses":{"200":{"description":"Updated user group detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserGroupDetail"}}}},"400":{"description":"Invalid group id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User group not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["User Groups"],"operationId":"deleteUserGroup","summary":"Delete user group","description":"Soft-delete a user group by setting deleted_date. The record is retained for audit purposes. Requires session auth and user-groups:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User group UUID"}],"responses":{"200":{"description":"Group deleted","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"400":{"description":"Invalid group id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User group not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/usergroups/{id}/members":{"put":{"tags":["User Groups"],"operationId":"updateGroupMembers","summary":"Add/remove group members","description":"Add and/or remove members from a user group in a single request. Requires session auth and user-groups:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User group UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGroupMembersBody"}}}},"responses":{"200":{"description":"Members updated — counts of added and removed records","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGroupMembersResponse"}}}},"400":{"description":"Invalid group id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User group not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/usergroups/{id}/roles":{"put":{"tags":["User Groups"],"operationId":"updateGroupRoles","summary":"Assign/remove group roles","description":"Assign and/or remove roles from a user group in a single request. Requires session auth and user-groups:WRITE permission.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User group UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGroupRolesBody"}}}},"responses":{"200":{"description":"Roles updated — counts of added and removed records","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGroupRolesResponse"}}}},"400":{"description":"Invalid group id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User group not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/users":{"get":{"tags":["Clients"],"operationId":"listClientUsers","summary":"List users with access to a client","description":"Returns every user who has a non-deleted user_role_link OR is a member of a user_group with a non-deleted user_group_role_link scoped to this client. Aggregated per user with role + permission summary.","security":[{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"List of users with access","content":{"application/json":{"schema":{"type":"object","required":["items","total","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"type":"object","additionalProperties":true}},"total":{"type":"integer"},"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Clients"],"operationId":"addClientUser","summary":"Add a user to a client (invite new or attach existing)","description":"Auto-detects by email. If the user exists, inserts a user_role_link for (user, client, role). If not, provisions a Firebase user + sends an invite with the role attached. role_id must be assignable for this client (not Maison-only, not disabled).","security":[{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","role_id"],"properties":{"email":{"type":"string","format":"email"},"role_id":{"type":"string","format":"uuid"},"first_name":{"type":"string"},"last_name":{"type":"string"}}}}}},"responses":{"200":{"description":"Existing user attached","content":{"application/json":{"schema":{"type":"object","required":["kind","user_id"],"properties":{"kind":{"type":"string","enum":["attached"]},"user_id":{"type":"string","format":"uuid"}}}}}},"201":{"description":"New user invited","content":{"application/json":{"schema":{"type":"object","required":["kind","user_id"],"properties":{"kind":{"type":"string","enum":["invited"]},"user_id":{"type":"string","format":"uuid"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Role is not assignable for this client","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Email already registered in Firebase","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/users/{userId}":{"delete":{"tags":["Clients"],"operationId":"removeClientUser","summary":"Remove all direct role-links for (user, client)","description":"Soft-deletes every active user_role_link for this user-client pair. Does NOT touch user_group memberships. Idempotent.","security":[{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Role links removed","content":{"application/json":{"schema":{"type":"object","required":["removed"],"properties":{"removed":{"type":"integer"}}}}}},"400":{"description":"Invalid client or user id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/usergroups":{"get":{"tags":["Clients"],"operationId":"listClientUserGroups","summary":"List user-groups with role links on this client","security":[{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"List of user-groups with role links","content":{"application/json":{"schema":{"type":"object","required":["items","total","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"type":"object","additionalProperties":true}},"total":{"type":"integer"},"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Clients"],"operationId":"addClientGroup","summary":"Attach an existing user group to this client with a role","description":"Adds a role link for (group, client). Group must already exist (this endpoint does not create groups). Role must be assignable for this client.","security":[{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["group_id","role_id"],"properties":{"group_id":{"type":"string","format":"uuid"},"role_id":{"type":"string","format":"uuid"}}}}}},"responses":{"201":{"description":"Group attached","content":{"application/json":{"schema":{"type":"object","required":["kind","group_id"],"properties":{"kind":{"type":"string","enum":["attached"]},"group_id":{"type":"string","format":"uuid"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Role is not assignable for this client","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User group not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/usergroups/{groupId}":{"delete":{"tags":["Clients"],"operationId":"removeClientUserGroup","summary":"Remove all role-links for (group, client)","security":[{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Role links removed","content":{"application/json":{"schema":{"type":"object","required":["removed"],"properties":{"removed":{"type":"integer"}}}}}},"400":{"description":"Invalid client or group id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/roles":{"get":{"tags":["Clients"],"operationId":"listClientAssignableRoles","summary":"List roles that can be assigned to users/groups for this client","description":"Excludes disabled roles and roles marked visibility_maison_only=true (Maison-admin-only). Returns both global roles and client-scoped roles belonging to this client.","security":[{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Assignable roles","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/regions":{"get":{"tags":["Settings"],"operationId":"listRegions","summary":"List regions","description":"Return a paginated, searchable list of regions for a client. Requires session auth and settings:READ permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of regions to return (1-100)"},{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response as `nextCursor`"},{"name":"search","in":"query","schema":{"type":"string","maxLength":100},"description":"Case-insensitive search string matched against region name"}],"responses":{"200":{"description":"Paginated list of regions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegionListResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Settings"],"operationId":"createRegion","summary":"Create region","description":"Create a new region for a client. Requires session auth and settings:WRITE permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRegionBody"}}}},"responses":{"201":{"description":"Region created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/regions/{id}":{"put":{"tags":["Settings"],"operationId":"updateRegion","summary":"Update region","description":"Partially update a region. All body fields are optional. Requires session auth and settings:WRITE permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Region UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRegionBody"}}}},"responses":{"200":{"description":"Updated region","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegionListItem"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Region not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Settings"],"operationId":"deleteRegion","summary":"Delete region","description":"Soft-delete a region by setting deleted_date. The record is retained for audit purposes. Requires session auth and settings:WRITE permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Region UUID"}],"responses":{"200":{"description":"Region deleted","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Region not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/departments":{"get":{"tags":["Settings"],"operationId":"listDepartments","summary":"List departments","description":"Return a paginated, searchable list of departments for a client. Requires session auth and settings:READ permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of departments to return (1-100)"},{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response as `nextCursor`"},{"name":"search","in":"query","schema":{"type":"string","maxLength":100},"description":"Case-insensitive search string matched against department name"}],"responses":{"200":{"description":"Paginated list of departments","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentListResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Settings"],"operationId":"createDepartment","summary":"Create department","description":"Create a new department for a client. Requires session auth and settings:WRITE permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDepartmentBody"}}}},"responses":{"201":{"description":"Department created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/departments/{id}":{"put":{"tags":["Settings"],"operationId":"updateDepartment","summary":"Update department","description":"Partially update a department. All body fields are optional. Requires session auth and settings:WRITE permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Department UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDepartmentBody"}}}},"responses":{"200":{"description":"Updated department","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentListItem"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Department not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Settings"],"operationId":"deleteDepartment","summary":"Delete department","description":"Soft-delete a department by setting deleted_date. The record is retained for audit purposes. Requires session auth and settings:WRITE permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Department UUID"}],"responses":{"200":{"description":"Department deleted","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Department not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/tags":{"get":{"tags":["Settings"],"operationId":"listTags","summary":"List tags","description":"Return a paginated, searchable list of tags for a client. Requires session auth and settings:READ permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of tags to return (1-100)"},{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response as `nextCursor`"},{"name":"search","in":"query","schema":{"type":"string","maxLength":100},"description":"Case-insensitive search string matched against tag name"}],"responses":{"200":{"description":"Paginated list of tags","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagListResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Settings"],"operationId":"createTag","summary":"Create tag","description":"Create a new tag for a client. Requires session auth and settings:WRITE permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTagBody"}}}},"responses":{"201":{"description":"Tag created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{clientId}/tags/{id}":{"put":{"tags":["Settings"],"operationId":"updateTag","summary":"Update tag","description":"Partially update a tag. All body fields are optional. Requires session auth and settings:WRITE permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Tag UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTagBody"}}}},"responses":{"200":{"description":"Updated tag","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagListItem"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Tag not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Settings"],"operationId":"deleteTag","summary":"Delete tag","description":"Soft-delete a tag by setting deleted_date. The record is retained for audit purposes. Requires session auth and settings:WRITE permission.","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Tag UUID"}],"responses":{"200":{"description":"Tag deleted","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Tag not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/children":{"get":{"tags":["Clients"],"operationId":"listClientChildren","summary":"List a client's child properties","description":"Return the UUIDs of the direct active child clients (properties) under the given parent. The hierarchy is a multi-level tree (e.g. global chain > regional > country > hotel); this returns direct children only, not the full subtree. Each client has at most one active parent. Requires global clients:READ (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Parent client UUID"}],"responses":{"200":{"description":"Active child client IDs","content":{"application/json":{"schema":{"type":"object","required":["children"],"properties":{"children":{"type":"array","items":{"type":"string","format":"uuid"},"description":"UUIDs of active child clients, ordered by attach date ASC"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to list children","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Clients"],"operationId":"attachChildClient","summary":"Attach a child property to a parent client","description":"Attaches childClientId as an active child of the given parent in the multi-level hierarchy tree. An intermediate node may be both a parent and a child (arbitrary depth). Invariants enforced: no self-attachment, no cycles (the child must not already be an ancestor of the parent), and each child may have at most one active parent. Detach and re-attach cycles are safe — old rows are soft-deleted and kept as history. Requires global clients:WRITE (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Parent client UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["childClientId"],"properties":{"childClientId":{"type":"string","format":"uuid","description":"UUID of the client to attach as a child property"}}}}}},"responses":{"204":{"description":"Child attached"},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Hierarchy conflict (self-attach, depth > 1, or child already has an active parent)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to attach child","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/children/{childid}":{"delete":{"tags":["Clients"],"operationId":"detachChildClient","summary":"Detach a child property from a parent client","description":"Soft-deletes the active parent–child link. No-op if no active link exists (safe to call multiple times). The detached child becomes a standalone client; the soft-deleted row is kept as membership history. Requires global clients:WRITE (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Parent client UUID"},{"name":"childid","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Child client UUID to detach"}],"responses":{"204":{"description":"Child detached (or was already standalone)"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to detach child","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/parent":{"get":{"tags":["Clients"],"operationId":"getClientParent","summary":"Get a client's parent property group","description":"Returns the UUID of the active parent client, or null when the client is a standalone property. Requires global clients:READ (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Parent client ID (null when standalone)","content":{"application/json":{"schema":{"type":"object","required":["parentClientId"],"properties":{"parentClientId":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"UUID of the active parent client, or null if standalone"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to get parent","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/handoff":{"post":{"tags":["Clients"],"operationId":"handoffToChildProperty","summary":"Resolve a guest's property selection and create a child session","description":"Guest-accessible — no auth required (mirrors POST /api/message). Resolves the guest's free-text property selection against the directory the source client can see and creates a new DynamoDB session for the resolved target. A ROUTING source (hierarchy.role 'parent' or 'intermediate' — any node with children) resolves against its own direct children. A LEAF source (role 'child' with a parentClientId) resolves against its group — every OTHER child of its parent plus the parent itself — so a property can hand the guest to a sister property or back to the group desk; the source's own name never resolves. A standalone client (no children and no parent) is a 404. The target session is always seeded with [system preamble, the carried source conversation, assistant greeting]: up to 10 alternating guest AND assistant rows, newest by time, deduped, excluding opening messages and the source's own PROPERTY_SELECTION transition reply. When pendingRequest is set and the target is a leaf, that question is dispatched as the new session's first guest turn in the background instead of being carried. Returns a discriminated status: 'resolved' (childClientId + childSessionId + hasChildren, where hasChildren=true means the target is itself a routing node that continues property selection — an intermediate group, or the group desk a leaf handed back to), 'ambiguous' (candidate list for re-prompt), 'notfound', or 'unavailable' (target resolved but currently disabled or deleted — authoritative SQL check).","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Source client UUID — a parent, intermediate, or child client; a child resolves against its group"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sessionId","selection"],"properties":{"sessionId":{"type":"string","description":"ID of the active source session — its conversation is carried into the new session. Must exist (404 otherwise), and a channel-derived id (32 hex chars) is rejected the same way."},"selection":{"type":"string","minLength":1,"description":"Guest's free-text property selection (name, alias, city, or clientId UUID)"},"pendingRequest":{"type":"string","maxLength":2000,"description":"Optional. The guest's not-yet-answered question at the moment of selection (trimmed and whitespace-collapsed; over 2000 characters is a 400). When set and the resolved target is a leaf property, it is excluded from the carried transcript and dispatched as the child session's first guest turn in the background — the handoff response returns immediately and the answer arrives over the child session's real-time channel (or on the next GET /api/message poll for a test session). Never dispatched when the resolved target is itself a routing node (hasChildren true), whose agent re-routes on its own next turn."},"locale":{"type":"string","maxLength":35,"pattern":"^[A-Za-z]{2,8}([-_][A-Za-z0-9]{1,8})*$","description":"Optional. The language the guest is reading in, as a platform locale token (`en_US` as the console writes catalogue locales, or `en-US` / `fr` as the agent reports them). The greeting seeded into the target session is read from that property's phrase catalogue in this language — the guest's locale first (exactly, then on its language subtag), then the property's default locale, then the built-in English wording. Anything outside the token shape is a 400. The system preamble is always English: it is context for the model, not copy for the guest."}}}}}},"responses":{"200":{"description":"Discriminated resolution result — check the 'status' field to determine the outcome","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["status","childClientId","childSessionId","hasChildren"],"properties":{"status":{"type":"string","enum":["resolved"]},"childClientId":{"type":"string","format":"uuid","description":"UUID of the resolved child property"},"childSessionId":{"type":"string","description":"ID of the newly created DynamoDB session scoped to the child property"},"hasChildren":{"type":"boolean","description":"True when the resolved child is itself a routing node (intermediate group) — the guest will continue property selection there; false = leaf property handoff."}}},{"type":"object","required":["status","candidates"],"properties":{"status":{"type":"string","enum":["ambiguous"]},"candidates":{"type":"array","description":"Two or more matching properties — re-prompt the guest","items":{"type":"object","required":["clientId","name"],"properties":{"clientId":{"type":"string","format":"uuid"},"name":{"type":"string"}}}}}},{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["notfound"]}}},{"type":"object","required":["status"],"description":"Child resolved but is currently disabled or deleted — authoritative SQL check.","properties":{"status":{"type":"string","enum":["unavailable"]}}}]}}}},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Source client not found; source is standalone (no children and no parent); the parent of a child source has no config; resolved target not found; or the source session does not exist","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process handoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/propertydiscovery":{"post":{"tags":["Clients"],"operationId":"startPropertyDiscovery","summary":"Start property discovery for a multi-property group website","description":"Marks the client as a multi-property group and enqueues the `discover-properties` pipeline task against its website. The task crawls the site (sitemap + homepage), groups the URLs into candidate properties, verifies them, and posts a proposal back; the run then sits at AWAITING_REVIEW until an operator approves it. Nothing is created and no knowledge base is written until approval. Returns 409 when a run is already DISCOVERING or PROVISIONING for this client, and 400 when the client has no website. Requires global clients:WRITE (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Parent (group) client UUID"}],"responses":{"202":{"description":"Discovery run accepted and enqueued","content":{"application/json":{"schema":{"type":"object","required":["session"],"properties":{"session":{"$ref":"#/components/schemas/PropertyDiscoverySession"}}}}}},"400":{"description":"Invalid client id, or the client has no website","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"A discovery run is already in progress for this client","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to start property discovery","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"get":{"tags":["Clients"],"operationId":"getLatestPropertyDiscovery","summary":"Get the latest property discovery run for a client","description":"Returns the most recent discovery run with its candidate properties, or `session: null` when the client has never run discovery (a normal state, not an error). Poll this while a run is DISCOVERING or PROVISIONING — the session status and each candidate's provisioning state are the progress record. Requires global clients:READ (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Parent (group) client UUID"}],"responses":{"200":{"description":"Latest discovery run, or null when there has never been one","content":{"application/json":{"schema":{"type":"object","required":["session"],"properties":{"session":{"oneOf":[{"$ref":"#/components/schemas/PropertyDiscoverySession"},{"type":"null"}]}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to read property discovery","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/propertydiscovery/{sessionid}":{"get":{"tags":["Clients"],"operationId":"getPropertyDiscoverySession","summary":"Get one property discovery run","description":"Returns a single discovery run and its candidates. The run is looked up by (client, session) together, so a session id belonging to another client reads as 404 rather than another client's data. Requires global clients:READ (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Parent (group) client UUID"},{"name":"sessionid","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Discovery session UUID"}],"responses":{"200":{"description":"The discovery run","content":{"application/json":{"schema":{"type":"object","required":["session"],"properties":{"session":{"$ref":"#/components/schemas/PropertyDiscoverySession"}}}}}},"400":{"description":"Invalid client or session id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Discovery session not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to read property discovery","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/propertydiscovery/{sessionid}/candidates/{candidateid}":{"patch":{"tags":["Clients"],"operationId":"updatePropertyDiscoveryCandidate","summary":"Edit, approve or reject one proposed property","description":"Applies an operator's review decision to one candidate. Any subset of the fields may be sent; omitted fields are left unchanged. Only allowed while the run is AWAITING_REVIEW — once provisioning starts the candidate rows are the provisioner's input, and a concurrent rename would produce a child client nobody approved (409). Requires global clients:WRITE (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Parent (group) client UUID"},{"name":"sessionid","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Discovery session UUID"},{"name":"candidateid","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Candidate property UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","minProperties":1,"properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"Display name for the child client that will be created"},"city":{"oneOf":[{"type":"string","maxLength":200},{"type":"null"}],"description":"City — written to the child's hotelConfig, where it becomes part of the parent's routing directory"},"country":{"oneOf":[{"type":"string","maxLength":200},{"type":"null"}]},"address":{"oneOf":[{"type":"string","maxLength":500},{"type":"null"}]},"urlPrefixes":{"type":"array","minItems":1,"items":{"type":"string","format":"uri"},"description":"URL prefixes that belong to this property. These become the child's scrape scope, and are excluded from the parent's."},"decision":{"type":"string","enum":["PENDING","APPROVED","REJECTED"]}}}}}},"responses":{"200":{"description":"The updated candidate","content":{"application/json":{"schema":{"type":"object","required":["candidate"],"properties":{"candidate":{"$ref":"#/components/schemas/PropertyDiscoveryCandidate"}}}}}},"400":{"description":"Invalid id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Discovery session or candidate not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"The run is no longer awaiting review","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to update candidate","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/propertydiscovery/{sessionid}/decisions":{"post":{"tags":["Clients"],"operationId":"setAllPropertyDiscoveryDecisions","summary":"Set the review decision on every candidate at once","description":"Applies one decision to every candidate in the run — the 'approve all' / 'reject all' affordance for a brand site with dozens of properties. Only allowed while the run is AWAITING_REVIEW. Requires global clients:WRITE (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Parent (group) client UUID"},{"name":"sessionid","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Discovery session UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["decision"],"properties":{"decision":{"type":"string","enum":["PENDING","APPROVED","REJECTED"]}}}}}},"responses":{"200":{"description":"Number of candidates updated","content":{"application/json":{"schema":{"type":"object","required":["updated"],"properties":{"updated":{"type":"integer"}}}}}},"400":{"description":"Invalid id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Discovery session not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"The run is no longer awaiting review","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to update candidates","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/propertydiscovery/{sessionid}/approve":{"post":{"tags":["Clients"],"operationId":"approvePropertyDiscovery","summary":"Provision the approved properties as child clients","description":"The human-in-the-loop gate. For every APPROVED candidate this creates a child client, scopes its knowledge base to that property's URLs, writes its hotelConfig (name/city/address — the fields the parent's routing directory reads), attaches it under the parent in the hierarchy, and enqueues its website scrape. The parent is then scoped to exclude every provisioned child's URLs and (unless rescrapeParent is false) re-scraped so its own knowledge base collapses to group/brand content.\n\nProvisioning runs DETACHED and this returns 202 immediately — a brand with dozens of properties takes far longer than an HTTP request may live. Poll GET /api/clients/{id}/propertydiscovery for progress: the session status moves PROVISIONING → COMPLETED/FAILED and each candidate carries its own provisionStatus and provisionError. Candidates fail independently; re-posting this endpoint retries only the ones that are not yet PROVISIONED. Requires global clients:WRITE (Maison-admin only).","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Parent (group) client UUID"},{"name":"sessionid","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Discovery session UUID"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"rescrapeParent":{"type":"boolean","default":true,"description":"Re-scrape the parent after narrowing its scope to group-level pages. Leave true unless the parent's knowledge base is curated by hand."}}}}}},"responses":{"202":{"description":"Provisioning accepted and running in the background","content":{"application/json":{"schema":{"type":"object","required":["status","sessionId","approved"],"properties":{"status":{"type":"string","enum":["PROVISIONING"]},"sessionId":{"type":"string","format":"uuid"},"approved":{"type":"integer","description":"Number of approved candidates this run will provision"}}}}}},"400":{"description":"Invalid id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Discovery session not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"The run is not awaiting review, or no candidates are approved","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to start provisioning","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/knowledgegaps":{"get":{"tags":["KB Gaps"],"operationId":"listKnowledgeGaps","summary":"List knowledge gaps","description":"Return a client's detected knowledge-base gaps (guest questions the AI couldn't answer) ordered by occurrence_count DESC, last_seen DESC, plus the latest detection run. Requires clients:READ (client-scoped).","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"},"description":"Client (tenant) UUID"},{"name":"status","in":"query","schema":{"type":"string","enum":["OPEN","ANSWERED","DISMISSED","CLOSED"]},"description":"Filter by gap lifecycle status. OPEN, ANSWERED and DISMISSED match a stored status exactly; CLOSED is a grouping that returns every resolved gap (ANSWERED + DISMISSED), for callers rendering a plain Open/Closed split. Unknown values are rejected with 400."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Maximum number of gaps to return (1-200)"},{"name":"paginationtoken","in":"query","schema":{"type":"string"},"description":"Opaque keyset cursor returned in the previous response"}],"responses":{"200":{"description":"knowledge gaps + latest detection run","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeGapListResponse"}}}},"400":{"description":"Invalid client id or query params","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/knowledgegaps/run":{"post":{"tags":["KB Gaps"],"operationId":"runKnowledgeGapDetection","summary":"Trigger knowledge-gap detection","description":"Start a MANUAL knowledge-gap detection run for the client. The run executes detached (it can take minutes); the endpoint responds 202 immediately. Requires clients:EXECUTE (client-scoped).","security":[{"apiKey":[]},{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"},"description":"Client (tenant) UUID"}],"responses":{"202":{"description":"Detection run queued","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/auth/":{"post":{"tags":["Legacy"],"operationId":"legacyBcAuthLogin","summary":"DEPRECATED: Legacy login","description":"DEPRECATED: Accepts legacy Basic-in-body credentials and issues a session cookie. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"username":{"type":"string"},"auth":{"type":"string"}}}}}},"responses":{"200":{"description":"Login successful"},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/auth/logout":{"post":{"tags":["Legacy"],"operationId":"legacyBcAuthLogout","summary":"DEPRECATED: Legacy logout","description":"DEPRECATED: Revokes the session cookie. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","responses":{"200":{"description":"Logged out"},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/client":{"post":{"tags":["Legacy"],"operationId":"legacyBcClientCreate","summary":"DEPRECATED: Create client","description":"DEPRECATED: Creates a new client with an associated website and hotel config, and deploys the demo preview site (best-effort; matches the legacy agent-server which deployed it on create). Consumer: legacy/business-console. When `multiProperty` is true the client is created as a multi-property GROUP: the create path enqueues property discovery (POST /api/clients/{id}/propertydiscovery) instead of a whole-site scrape, so the group's knowledge base is never hydrated with its children's pages. When `manualKnowledge` is true no scrape is triggered at all.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","website"],"properties":{"name":{"type":"string"},"demoClient":{"type":"boolean"},"website":{"type":"string","format":"uri"},"previewSiteConfiguration":{"type":"object"},"hotelConfig":{"type":"object"},"scrapeLanguage":{"type":"string"},"manualKnowledge":{"type":"boolean","default":false,"description":"Mark the knowledge base as manually curated — suppresses the scrape on create and the scheduled website-sync fan-out"},"multiProperty":{"type":"boolean","default":false,"description":"This website is a multi-property group/brand site. Runs property discovery on create instead of a whole-site scrape."}}}}}},"responses":{"201":{"description":"Created — { client: { id } }"},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"get":{"tags":["Legacy"],"operationId":"legacyBcClientGet","summary":"DEPRECATED: Get client","description":"DEPRECATED: Returns the client identified by x-client-id header. Consumer: legacy/business-console. The `websiteScrapeStatus` object includes an optional `message: string | null` field populated from `meta.abortMessage` when the session was aborted by an operator (meta.aborted === true); null otherwise.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"responses":{"200":{"description":"Client record"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Legacy"],"operationId":"legacyBcClientUpdate","summary":"DEPRECATED: Update client","description":"DEPRECATED: Updates the client identified by x-client-id header. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated client. When the request carried pmsConfig and it was persisted, the response also carries pmsConfig with every credential masked (xxxxxx + last 4 characters) — plaintext is never returned."},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/client/list":{"get":{"tags":["Legacy"],"operationId":"legacyBcClientList","summary":"DEPRECATED: List clients","description":"DEPRECATED: Returns all active clients as an array. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"responses":{"200":{"description":"Array of clients"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/client/summary":{"get":{"tags":["Legacy"],"operationId":"legacyBcClientSummary","summary":"DEPRECATED: Get client summary","description":"DEPRECATED: Returns minimal client summary. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"responses":{"200":{"description":"Client summary"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/hotelconfiguration":{"get":{"tags":["Legacy"],"operationId":"legacyBcHotelConfigurationGet","summary":"Legacy: Get hotel configuration","description":"Legacy: Returns the hotel configuration object for the client. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Hotel configuration object"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Legacy"],"operationId":"legacyBcHotelConfiguration","summary":"Legacy: Update hotel configuration","description":"Legacy: Merges the provided fields into the hotel configuration for the client. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated hotel configuration"},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Legacy"],"operationId":"legacyBcHotelConfigurationDelete","summary":"Legacy: Remove hotel configuration fields","description":"Legacy: Removes specified top-level fields from the hotel configuration for the client. Field names may be provided via x-configuration-fields header (comma-separated) or body.fields array. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}},{"name":"x-configuration-fields","in":"header","required":false,"schema":{"type":"string"},"description":"Comma-separated field names to remove (alternative to body.fields)"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Updated hotel configuration after field removal"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/website":{"post":{"tags":["Legacy"],"operationId":"legacyBcWebsiteCreate","summary":"Legacy: Add website knowledge source","description":"Legacy: Resolves or inserts a websites row, links it to the client, optionally promotes it as the primary website, and kicks off an initial scrape. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["domain"],"properties":{"domain":{"type":"string","format":"uri"},"updateClientWebsite":{"type":"boolean"},"syncSchedule":{"type":"string","enum":["NONE","DAILY","WEEKLY","BI_WEEKLY","MONTHLY"]},"language":{"type":"string"}}}}}},"responses":{"201":{"description":"Website source created and scrape queued"},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Website already linked to client","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Knowledge source storage not provisioned","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Legacy"],"operationId":"legacyBcWebsiteUpdate","summary":"Legacy: Update primary website sync schedule","description":"Legacy: Updates the syncSchedule for the primary website source of the client. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated"},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/client/source/list":{"get":{"tags":["Legacy"],"operationId":"legacyBcClientSourceList","summary":"DEPRECATED: List client knowledge sources","description":"DEPRECATED: Returns knowledge source links for the client. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"includedeleted","in":"query","required":false,"schema":{"type":"boolean"},"description":"When true, includes soft-deleted source links. Default: false."}],"responses":{"200":{"description":"Array of source links"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/website/{id}/sync":{"put":{"tags":["Legacy"],"operationId":"legacyBcWebsiteSync","summary":"DEPRECATED: Trigger website sync","description":"DEPRECATED: Returns 202 Accepted; actual sync is pipeline-side. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Sync queued"},"400":{"description":"Invalid website id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/website/{id}/bookinglinks":{"put":{"tags":["Legacy"],"operationId":"legacyBcWebsiteDiscoverBookingLinks","summary":"DEPRECATED: Re-run booking-link discovery for a website","description":"DEPRECATED: Returns 202 Accepted; discovery runs pipeline-side and refreshes the client's booking-instruction config entries. Idempotent — an unchanged booking link skips both instruction generation and the config write, so re-driving a hotel cannot clobber hand-tuned instruction text. Requires document-source WRITE. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Booking-link discovery queued"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/qa/counts":{"get":{"tags":["Legacy"],"operationId":"legacyQaCounts","summary":"DEPRECATED: QA pair counts","description":"DEPRECATED: Returns active/deleted/total counts for a client's QA namespace. Consumer: legacy/qa-editor.","deprecated":true,"x-legacy-consumer":"qa-editor","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"QA pair counts","content":{"application/json":{"schema":{"type":"object","properties":{"active":{"type":"integer"},"deleted":{"type":"integer"},"total":{"type":"integer"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/qa/deduplicate":{"post":{"tags":["Legacy"],"operationId":"legacyBcQaDeduplicate","summary":"DEPRECATED: Deduplicate QA","description":"DEPRECATED: Enqueues a deduplicate-kb pipeline task. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"string"},"similarityThreshold":{"type":"number"},"batchSize":{"type":"integer"}}}}}},"responses":{"202":{"description":"Queued","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean"},"messageId":{"type":"string","nullable":true}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/qa/consistencycheck":{"post":{"tags":["Legacy"],"operationId":"legacyBcQaConsistencyCheck","summary":"DEPRECATED: QA consistency check","description":"DEPRECATED: Enqueues a check-consistency pipeline task. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"string"}}}}}},"responses":{"202":{"description":"Queued","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean"},"messageId":{"type":"string","nullable":true}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/knowledgegaps":{"get":{"tags":["Legacy"],"operationId":"legacyKnowledgeGapsList","summary":"DEPRECATED: List knowledge gaps","description":"DEPRECATED: Returns the client's knowledge gaps for the qa-editor flyout 'Unanswered questions' tab (defaults to OPEN). Gated by qa-editor:READ + the unanswered-questions entitlement. Consumer: legacy/qa-editor. New clients use GET /api/knowledgegaps.","deprecated":true,"x-legacy-consumer":"qa-editor","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"},"description":"Client (tenant) UUID"},{"name":"status","in":"query","schema":{"type":"string","enum":["OPEN","ANSWERED","DISMISSED"],"default":"OPEN"},"description":"Filter by gap lifecycle status. Unknown values are rejected with 400."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"paginationtoken","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"knowledge gaps","content":{"application/json":{"schema":{"type":"object","required":["gaps"],"properties":{"gaps":{"type":"array","items":{"$ref":"#/components/schemas/KnowledgeGap"}},"paginationtoken":{"type":"string"}}}}}},"400":{"description":"Invalid client id or query params","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions or feature not enabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/knowledgegaps/{id}/answer":{"post":{"tags":["Legacy"],"operationId":"legacyKnowledgeGapsAnswer","summary":"DEPRECATED: Answer a knowledge gap","description":"DEPRECATED: Creates a QA pair in llm-service (question = the gap's canonical question) then marks the gap ANSWERED. 409 when the gap is no longer OPEN (no QA pair is created). Gated by qa-editor:WRITE + the unanswered-questions entitlement. Consumer: legacy/qa-editor.","deprecated":true,"x-legacy-consumer":"qa-editor","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"},"description":"Client (tenant) UUID"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"knowledge gap UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeGapAnswerBody"}}}},"responses":{"200":{"description":"Gap answered","content":{"application/json":{"schema":{"type":"object","required":["answered"],"properties":{"answered":{"type":"boolean"},"qaId":{"type":["string","null"]}}}}}},"400":{"description":"Invalid client id, gap id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions or feature not enabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"knowledge gap not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"knowledge gap is no longer OPEN","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"QA service unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/knowledgegaps/{id}/dismiss":{"post":{"tags":["Legacy"],"operationId":"legacyKnowledgeGapsDismiss","summary":"DEPRECATED: Dismiss a knowledge gap","description":"DEPRECATED: Marks an OPEN gap DISMISSED with resolved_by/resolved_date. 409 when the gap is no longer OPEN. Gated by qa-editor:WRITE + the unanswered-questions entitlement. Consumer: legacy/qa-editor.","deprecated":true,"x-legacy-consumer":"qa-editor","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"},"description":"Client (tenant) UUID"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"knowledge gap UUID"}],"responses":{"200":{"description":"Gap dismissed","content":{"application/json":{"schema":{"type":"object","required":["dismissed"],"properties":{"dismissed":{"type":"boolean"}}}}}},"400":{"description":"Invalid client id or gap id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Insufficient permissions or feature not enabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"knowledge gap not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"knowledge gap is no longer OPEN","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/client/asset":{"post":{"tags":["Legacy"],"operationId":"legacyBcClientAsset","summary":"DEPRECATED: Upload client asset","description":"DEPRECATED: Uploads an image asset for the client. Named visual-branding uploads (assetType=icon|hero|heroDark) replace the corresponding branding asset; library uploads (no assetType) append to the client asset list. Returns the stored asset URL. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"Raster image to upload. The type is validated from the file's magic bytes (PNG, JPEG, GIF, or WEBP only); SVG and other/spoofed types are rejected with 400. The stored extension is derived from the detected type, not the filename."},"assetType":{"type":"string","enum":["icon","hero","heroDark"],"description":"Visual-branding asset type. Omit for general library upload."},"description":{"type":"string","description":"Optional description (library uploads only)"},"published":{"type":"boolean","description":"Published flag (library uploads only)"}}}}}},"responses":{"200":{"description":"Asset uploaded","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"asset_url":{"type":"string","description":"URL of the stored asset"}}}}}},"400":{"description":"Invalid client id / Missing file / Unknown asset type","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Asset storage not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Legacy"],"operationId":"legacyBcClientAssetDelete","summary":"DEPRECATED: Delete client asset","description":"DEPRECATED: Deletes a named visual-branding asset (?assetType=icon|hero|heroDark) or a general library asset (?assetType=<id>). Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"assetType","in":"query","required":true,"schema":{"type":"string"},"description":"Visual-branding type (icon|hero|heroDark) or library asset id"}],"responses":{"200":{"description":"Asset deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Invalid client id / Missing asset type","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/translations":{"put":{"tags":["Legacy"],"operationId":"legacyBcTranslationsUpdate","summary":"DEPRECATED: Update translations","description":"DEPRECATED: Updates UI translations for the client; returns 204. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"Accepted"},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/analytics/chatlogs":{"post":{"tags":["Legacy"],"operationId":"legacyBcAnalyticsChatlogs","summary":"DEPRECATED: Trigger chat-logs analytics","description":"DEPRECATED: Queues a chat-log analytics report for the specified date range. The response returns immediately; report generation and email delivery happen in the background — failures are recorded on the analytics run status, not the HTTP response. Custom mode when recipients.to is supplied; scheduled mode otherwise. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"string","description":"Client UUID (falls back to x-client-id header)"},"startDate":{"type":"string","format":"date-time","description":"Start of date range (ISO 8601). Defaults to 8 days ago."},"endDate":{"type":"string","format":"date-time","description":"End of date range (ISO 8601). Defaults to 24 hours ago."},"recipients":{"type":"object","description":"Custom recipients. When present triggers custom mode (no unsubscribe). to is required.","required":["to"],"properties":{"to":{"type":"array","items":{"type":"string","format":"email"}},"cc":{"type":"array","items":{"type":"string","format":"email"}},"bcc":{"type":"array","items":{"type":"string","format":"email"}}}}}}}}},"responses":{"202":{"description":"Accepted — report queued; delivered to the recipients by email when ready","content":{"application/json":{"schema":{"type":"object","properties":{"analyticsId":{"type":"string"},"queued":{"type":"boolean"}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"422":{"description":"No chat logs found for the specified date range","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/logs/chat":{"get":{"tags":["Legacy"],"operationId":"legacyBcLogsChat","summary":"DEPRECATED: Download chat logs as XLSX","description":"DEPRECATED: Streams an XLSX download of raw chat-log messages for a client in the given date window. Returns 204 when no logs match. The sheet ends with three columns: channel (the session's channel; for a non-channel session its source, e.g. web; blank on legacy rows without provenance), routed_from (the name of the client that owns the connection that carried the session, filled only when that client is not the exporting client; blank otherwise) and routed_to (the name of the child hotel a conversation was routed to; blank on the client's own rows). When the exporting client owns shared channel accounts, the export also includes, after its own rows, the conversations those accounts carried on every child hotel they ever routed to, under the same date window and skip flags; a client with no accounts gets exactly the export it got before. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"startDate","in":"query","required":true,"schema":{"type":"string","format":"date-time"},"description":"ISO 8601 window start"},{"name":"endDate","in":"query","required":true,"schema":{"type":"string","format":"date-time"},"description":"ISO 8601 window end"},{"name":"skipFresh","in":"query","required":false,"schema":{"type":"string"},"description":"true (default) omits first-visit sessions"},{"name":"skipAdmin","in":"query","required":false,"schema":{"type":"string"},"description":"true (default) omits admin sessions"},{"name":"skipTestSessions","in":"query","required":false,"schema":{"type":"string"},"description":"true (default) omits test sessions"}],"responses":{"200":{"description":"XLSX workbook download","headers":{"Content-Disposition":{"description":"attachment; filename=\"<slug>-chatlogs-<start>-<end>.xlsx\"","schema":{"type":"string"}}},"content":{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}}}},"204":{"description":"No chat logs match the date range"},"400":{"description":"Invalid request (client id / date range)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/analytics/aivisibility":{"post":{"tags":["Legacy"],"operationId":"legacyBcAnalyticsAiVisibility","summary":"DEPRECATED: Trigger AI visibility analytics","description":"DEPRECATED: Enqueues a run-ai-visibility pipeline task. sessionId generated server-side if absent. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"string"},"sessionId":{"type":"string","description":"Session ID; generated server-side if absent"}}}}}},"responses":{"202":{"description":"Queued","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean"},"messageId":{"type":"string","nullable":true},"sessionId":{"type":"string"},"noAddress":{"type":"boolean","description":"True when the hotel has no address on file; the Business Console warns the operator at trigger time (AI Visibility results are less accurate without it)."}}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/test/session/list":{"get":{"tags":["Legacy"],"operationId":"legacyBcTestSessionList","summary":"DEPRECATED: List test sessions","description":"DEPRECATED: Returns test sessions from the testing schema. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"responses":{"200":{"description":"Array of test sessions"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/test/session/report":{"get":{"tags":["Legacy"],"operationId":"legacyBcTestSessionReport","summary":"DEPRECATED: Get test session report","description":"DEPRECATED: Returns the test session report object { id, description, tests[] } for a given session_id (accepts ?id= or ?session_id=). Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"session_id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Test session report ({ id, description, tests[] })"},"400":{"description":"Missing session_id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Test session not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/test/session/gfaq":{"post":{"tags":["Legacy"],"operationId":"legacyBcTestSessionGfaq","summary":"DEPRECATED: Generate FAQ from test session","description":"DEPRECATED: Returns 202 Accepted; FAQ generation is pipeline-side. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"202":{"description":"Queued"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/subscription":{"post":{"tags":["Subscriptions"],"operationId":"subscriptionCreate","summary":"Create email subscribers (bulk)","description":"Inserts one or more rows into email_subscribers. Resolves known users by email (case-insensitive). Requires session auth.","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["subscribers"],"properties":{"subscribers":{"type":"array","minItems":1,"items":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"emailType":{"type":"string"},"email_type":{"type":"string"},"clientId":{"type":"string"},"client_id":{"type":"string"}}}}}}}}},"responses":{"201":{"description":"Subscribers created"},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Subscriptions"],"operationId":"subscriptionDelete","summary":"Soft-delete subscribers by id","description":"Soft-deletes email_subscriber rows. Pass comma-separated UUIDs in the ?ids= query param. Requires session auth.","security":[{"sessionAuth":[]}],"parameters":[{"in":"query","name":"ids","required":true,"schema":{"type":"string"},"description":"Comma-separated subscriber UUIDs"}],"responses":{"200":{"description":"Subscribers deleted"},"400":{"description":"Missing ids query parameter","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/subscription/list":{"get":{"tags":["Subscriptions"],"operationId":"subscriptionList","summary":"List active subscribers for a client","description":"Returns active (non-deleted, non-unsubscribed) email_subscribers for the x-client-id client. Requires session auth.","security":[{"sessionAuth":[]}],"parameters":[{"in":"header","name":"x-client-id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Array of subscribers"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/subscription/unsubscribe":{"post":{"tags":["Subscriptions"],"operationId":"subscriptionUnsubscribe","summary":"Token-based email unsubscribe","description":"Public endpoint — no auth required. Sets unsubscribed_date on the email_subscribers row identified by the ?id= UUID.","parameters":[{"in":"query","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Unsubscribed or already unsubscribed"},"400":{"description":"Invalid subscription id"},"404":{"description":"Subscription not found"},"500":{"description":"Server error"}}}},"/api/legacy/user/":{"get":{"tags":["Legacy"],"operationId":"legacyBcUserGet","summary":"Legacy: Get current user","description":"Legacy: Returns the authenticated user record. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"responses":{"200":{"description":"User record"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Legacy"],"operationId":"legacyBcUserUpdate","summary":"Legacy: Update user profile","description":"Legacy: Updates firstName, lastName, or phone for the specified user id. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid"},"firstName":{"type":"string"},"lastName":{"type":"string"},"phone":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated user record"},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/user/create":{"post":{"tags":["Legacy"],"operationId":"legacyBcUserCreate","summary":"DEPRECATED: Create user","description":"DEPRECATED: Inserts a minimal user record. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"}}}}}},"responses":{"201":{"description":"User created"},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Duplicate email","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/user/adminsetpassword":{"put":{"tags":["Legacy"],"operationId":"legacyBcUserAdminSetPassword","summary":"DEPRECATED: Admin set password","description":"DEPRECATED: Flags force_reset_password on the user record. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string"},"new_password":{"type":"string"}}}}}},"responses":{"200":{"description":"Password flagged for reset"},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/user/addroles":{"post":{"tags":["Legacy"],"operationId":"legacyBcUserAddRoles","summary":"DEPRECATED: Add roles to user","description":"DEPRECATED: Inserts user_role_links. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string"},"role_ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Roles added"},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/user/list":{"get":{"tags":["Legacy"],"operationId":"legacyBcUserList","summary":"DEPRECATED: List users","description":"DEPRECATED: Returns up to 100 users. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"responses":{"200":{"description":"Array of users"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/user/role/list":{"get":{"tags":["Legacy"],"operationId":"legacyBcUserRoleList","summary":"DEPRECATED: List roles","description":"DEPRECATED: Returns all active roles for dropdown population. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"responses":{"200":{"description":"Array of roles"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/salesagent":{"get":{"tags":["Legacy"],"operationId":"legacySalesAgentGet","summary":"Get sales agent config","description":"DEPRECATED: Returns the sales agent config (enabled flag + opportunities). Consumer: legacy/business-console, legacy/qa-editor.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sales agent config","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"opportunities":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to read sales agent config","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Legacy"],"operationId":"legacySalesAgentPut","summary":"Update sales agent config","description":"DEPRECATED: Updates the sales agent config. Disabling keeps the configured opportunities (never deletes them; restored on re-enable). Consumer: legacy/business-console, legacy/qa-editor.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"opportunities":{"type":"array","items":{"type":"object"}}}}}}},"responses":{"200":{"description":"Updated sales agent config"},"400":{"description":"No fields to update / Invalid enabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to update sales agent config","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/salesagent/whitelist":{"get":{"tags":["Legacy"],"operationId":"legacySalesAgentWhitelistGet","summary":"Get sales agent email whitelist","description":"DEPRECATED: Returns active SALES_AGENT_LEAD whitelist entries. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Active whitelist","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to read sales agent whitelist","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Legacy"],"operationId":"legacySalesAgentWhitelistPost","summary":"Add emails to sales agent whitelist","description":"DEPRECATED: Inserts new email rows (deduplication enforced). Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["emails"],"properties":{"emails":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Updated whitelist with added count"},"400":{"description":"No valid emails to add","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to add whitelist entries","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Legacy"],"operationId":"legacySalesAgentWhitelistDelete","summary":"Soft-delete whitelist entries","description":"DEPRECATED: Stamps deleted_date/deleted_by on the specified rows. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Remaining active whitelist"},"400":{"description":"Provide one or more ids","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to delete whitelist entries","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/salesagent/recipients":{"get":{"tags":["Legacy"],"operationId":"legacySalesAgentRecipients","summary":"Get sales agent email recipients","description":"DEPRECATED: Returns {email} list from active SALES_AGENT_LEAD whitelist. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Recipient list","content":{"application/json":{"schema":{"type":"object","properties":{"recipients":{"type":"array","items":{"type":"object","properties":{"email":{"type":"string"}}}}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to read sales agent recipients","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/branding":{"get":{"tags":["Legacy"],"operationId":"legacyBrandingGet","summary":"Get branding config","description":"DEPRECATED: Returns the branding config (enabled flag). Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Branding config","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to read branding config","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Legacy"],"operationId":"legacyBrandingPut","summary":"Update branding config","description":"DEPRECATED: Updates the branding config. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated"},"400":{"description":"Invalid enabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to update branding config","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/documents/config":{"get":{"tags":["Legacy"],"operationId":"legacyDocumentsConfigGet","summary":"Get documents config","description":"DEPRECATED: Returns the documents config (enabled flag). Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Documents config","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to read documents config","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Legacy"],"operationId":"legacyDocumentsConfigPut","summary":"Update documents config","description":"DEPRECATED: Updates the documents config. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated"},"400":{"description":"Invalid enabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to update documents config","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/client/chatstyles":{"get":{"tags":["Legacy"],"operationId":"legacyChatStylesGet","summary":"Get chat styles","description":"DEPRECATED: Returns the chat styles config map. Consumer: legacy/business-console, legacy/qa-editor.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Chat styles map","content":{"application/json":{"schema":{"type":"object","properties":{"chatStyles":{"type":"object"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to read chat styles","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Legacy"],"operationId":"legacyChatStylesPut","summary":"Update chat styles","description":"DEPRECATED: Updates the chat styles config map. Consumer: legacy/business-console, legacy/qa-editor.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["chatStyles"],"properties":{"chatStyles":{"type":"object"}}}}}},"responses":{"200":{"description":"Updated chat styles"},"400":{"description":"Invalid chatStyles","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to update chat styles","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/webhooks/instagram":{"get":{"tags":["Webhooks"],"operationId":"instagramWebhookVerify","summary":"Meta webhook verification handshake","description":"Called by Meta to verify the callback URL. Echoes hub.challenge when hub.mode=subscribe and hub.verify_token matches the configured secret. Returns 403 otherwise.","parameters":[{"name":"hub.mode","in":"query","required":true,"schema":{"type":"string"},"description":"Must be 'subscribe'"},{"name":"hub.challenge","in":"query","required":true,"schema":{"type":"string"},"description":"Random string Meta expects to be echoed back"},{"name":"hub.verify_token","in":"query","required":true,"schema":{"type":"string"},"description":"Shared secret set in the Meta app dashboard"}],"responses":{"200":{"description":"Verification accepted — raw hub.challenge echoed as plain text","content":{"text/plain":{"schema":{"type":"string"}}}},"403":{"description":"Forbidden — mode is not subscribe or token does not match","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Webhooks"],"operationId":"instagramWebhookReceive","summary":"Receive Instagram DM webhook events","description":"Receives inbound Instagram DM events from Meta. Verifies the X-Hub-Signature-256 HMAC over the raw request body before processing. Distinguishes guest messages from operator message_echoes. Responds 200 immediately (fast ack). Phase 2a Sprint 1: transport only — no agent wiring.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["instagram"]},"entry":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Hotel Instagram Business Account ID"},"messaging":{"type":"array","items":{"type":"object","properties":{"sender":{"type":"object","properties":{"id":{"type":"string"}}},"recipient":{"type":"object","properties":{"id":{"type":"string"}}},"timestamp":{"type":"integer"},"message":{"type":"object","properties":{"mid":{"type":"string"},"text":{"type":"string"},"is_echo":{"type":"boolean","description":"true for operator message_echoes; absent/false for guest messages"}}}}}}}}}}}}}},"responses":{"200":{"description":"Webhook acknowledged","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"description":"Empty body or malformed JSON (after valid signature)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Missing or invalid X-Hub-Signature-256","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/webhooks/messenger":{"get":{"tags":["Webhooks"],"operationId":"messengerWebhookVerify","summary":"Meta Messenger webhook verification handshake","description":"Called by Meta to verify the Messenger callback URL. Echoes hub.challenge when hub.mode=subscribe and hub.verify_token matches. Returns 403 otherwise. Shares the same handler as /webhooks/instagram.","parameters":[{"name":"hub.mode","in":"query","required":true,"schema":{"type":"string"},"description":"Must be 'subscribe'"},{"name":"hub.challenge","in":"query","required":true,"schema":{"type":"string"}},{"name":"hub.verify_token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Verification accepted — raw hub.challenge echoed as plain text","content":{"text/plain":{"schema":{"type":"string"}}}},"403":{"description":"Forbidden — mode is not subscribe or token does not match","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Webhooks"],"operationId":"messengerWebhookReceive","summary":"Receive Facebook Messenger webhook events","description":"Receives inbound Facebook Messenger page events from Meta. Verifies X-Hub-Signature-256 HMAC against META_APP_SECRET. Routes events by object field: 'page' dispatches to the Messenger handler. Responds 200 immediately.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["page"]},"entry":{"type":"array","items":{"type":"object"}}}}}}},"responses":{"200":{"description":"Webhook acknowledged","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"description":"Empty body or malformed JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Missing or invalid X-Hub-Signature-256","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"META_APP_SECRET not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/webhooks/whatsapp":{"get":{"tags":["Webhooks"],"operationId":"whatsappWebhookVerify","summary":"Meta WhatsApp webhook verification handshake","description":"Called by Meta to verify the WhatsApp callback URL. Echoes hub.challenge when hub.mode=subscribe and hub.verify_token matches. Returns 403 otherwise. Shares the same handler as /webhooks/instagram.","parameters":[{"name":"hub.mode","in":"query","required":true,"schema":{"type":"string"},"description":"Must be 'subscribe'"},{"name":"hub.challenge","in":"query","required":true,"schema":{"type":"string"}},{"name":"hub.verify_token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Verification accepted — raw hub.challenge echoed as plain text","content":{"text/plain":{"schema":{"type":"string"}}}},"403":{"description":"Forbidden — mode is not subscribe or token does not match","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Webhooks"],"operationId":"whatsappWebhookReceive","summary":"Receive WhatsApp Business webhook events","description":"Receives inbound WhatsApp Business Account events from Meta. Verifies X-Hub-Signature-256 HMAC. Routes events by object field: 'whatsapp_business_account' dispatches to the WhatsApp handler. Responds 200 immediately.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["whatsapp_business_account"]},"entry":{"type":"array","items":{"type":"object"}}}}}}},"responses":{"200":{"description":"Webhook acknowledged","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"description":"Empty body or malformed JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Missing or invalid X-Hub-Signature-256","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"INSTAGRAM_APP_SECRET not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/dashboard/stats":{"get":{"tags":["REST API"],"operationId":"getDashboardStats","summary":"Get dashboard statistics","description":"Returns aggregate statistics for the authenticated user's dashboard view, scoped by x-client-id.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Dashboard statistics"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/dashboard/activity":{"get":{"tags":["REST API"],"operationId":"getDashboardActivity","summary":"Get dashboard activity feed","description":"Returns recent activity items for the authenticated user's dashboard, scoped by x-client-id. Supports pagination via limit (1–50, default 10).","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":10}}],"responses":{"200":{"description":"Activity feed","content":{"application/json":{"schema":{"type":"object","properties":{"activity":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Invalid client id or query parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/knowledge":{"get":{"tags":["REST API"],"operationId":"listKnowledge","summary":"List knowledge base items","description":"Returns a paginated list of knowledge base records for the specified client. Supports cursor-based pagination, full-text search, and source/date filters.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"clientId","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID to scope the query"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"after","in":"query","required":false,"schema":{"type":"string"},"description":"Cursor for pagination"},{"name":"includeDeleted","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"search","in":"query","required":false,"schema":{"type":"string"},"description":"Full-text search query"},{"name":"filter_source","in":"query","required":false,"schema":{"type":"string","enum":["Website","Document","User"]}},{"name":"filter_created_from","in":"query","required":false,"schema":{"type":"number"},"description":"Epoch ms lower bound for created_date"},{"name":"filter_created_to","in":"query","required":false,"schema":{"type":"number"},"description":"Epoch ms upper bound for created_date"}],"responses":{"200":{"description":"Knowledge item list","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}},"paginationToken":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}}}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/experiences":{"get":{"tags":["REST API"],"operationId":"listExperiences","summary":"List experiences","description":"Returns a paginated list of experiences for the authenticated client. Supports cursor-based pagination and sorting.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"after","in":"query","required":false,"schema":{"type":"string"},"description":"Cursor for pagination"}],"responses":{"200":{"description":"Experience list"},"400":{"description":"Invalid client id or query parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["REST API"],"operationId":"createExperience","summary":"Create an experience","description":"Creates a new experience for the client identified by x-client-id.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","category"],"properties":{"title":{"type":"string"},"category":{"type":"string","enum":["wellness","activity","excursion","rental","transfer"]},"shortDesc":{"type":"string"},"longDesc":{"type":"string"},"basePrice":{"type":"number"},"currency":{"type":"string"},"location":{"type":"string"},"duration":{"type":"integer"},"inventoryModel":{"type":"string","enum":["slots","per_unit"]},"slots":{"type":"integer"},"slotCapacity":{"type":"integer"},"daysOpen":{"type":"array","items":{"type":"string"}},"leadTime":{"type":"integer"},"partySize":{"type":"object"},"cancellation":{"type":"object"},"includes":{"type":"array","items":{"type":"string"}},"bring":{"type":"array","items":{"type":"string"}},"knowBefore":{"type":"array","items":{"type":"string"}},"aiNotes":{"type":"string"},"meetingType":{"type":"string"},"meetingDetail":{"type":"string"},"unitsTotal":{"type":"integer"},"operatingHoursOpen":{"type":"string"},"operatingHoursClose":{"type":"string"},"propertyId":{"type":"string","format":"uuid"}}}}}},"responses":{"201":{"description":"Experience created"},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/experiences/agent":{"post":{"tags":["REST API"],"operationId":"experienceAgent","summary":"Send a message to the experience agent","description":"Sends a message to the AI experience agent with context about catalog, detail, bookings, guests, or inbox. Returns an agent response. Requires experiences:READ permission.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["message","context"],"properties":{"message":{"type":"string","minLength":1,"maxLength":5000},"context":{"type":"string","enum":["catalog","detail","bookings","guests","inbox"]},"experienceId":{"type":"string","format":"uuid"},"attachmentUrl":{"type":"string","format":"uri"}}}}}},"responses":{"200":{"description":"Agent response"},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/experiences/{id}":{"get":{"tags":["REST API"],"operationId":"getExperience","summary":"Get an experience","description":"Returns the full detail of a single experience by its UUID.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Experience detail"},"400":{"description":"Invalid experience id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Experience not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["REST API"],"operationId":"updateExperience","summary":"Update an experience","description":"Updates the fields of an existing experience.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated experience"},"400":{"description":"Invalid experience id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Experience not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["REST API"],"operationId":"archiveExperience","summary":"Archive (soft-delete) an experience","description":"Soft-deletes an experience. The record remains in the database but is hidden from listings.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Experience archived"},"400":{"description":"Invalid experience id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Experience not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/experiences/{id}/publish":{"post":{"tags":["REST API"],"operationId":"publishExperience","summary":"Publish an experience","description":"Marks an experience as published. Returns 422 if the experience does not meet the sellable gate requirements (missing required fields for booking).","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Experience published"},"400":{"description":"Invalid experience id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Experience not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"422":{"description":"Experience does not meet sellable gate requirements","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/experiences/{id}/unpublish":{"post":{"tags":["REST API"],"operationId":"unpublishExperience","summary":"Unpublish an experience","description":"Marks an experience as unpublished (draft). Hides it from guest-facing surfaces.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Experience unpublished"},"400":{"description":"Invalid experience id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Experience not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/experiences/{id}/variants":{"post":{"tags":["REST API"],"operationId":"addExperienceVariant","summary":"Add a variant to an experience","description":"Adds a pricing variant to an experience (e.g. Early Bird, Standard, Premium).","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label","price"],"properties":{"label":{"type":"string"},"price":{"type":"number"},"default":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Variant added"},"400":{"description":"Invalid experience id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Experience not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/experiences/{id}/customertypes":{"post":{"tags":["REST API"],"operationId":"addExperienceCustomerType","summary":"Add a customer type to an experience","description":"Adds a customer type (Adult, Child, Senior, etc.) with its own pricing to an experience.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label"],"properties":{"label":{"type":"string"},"ageRange":{"type":"object"},"price":{"type":"number"},"maxPerBooking":{"type":"integer"}}}}}},"responses":{"201":{"description":"Customer type added"},"400":{"description":"Invalid experience id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Experience not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/experiences/{id}/availability":{"get":{"tags":["REST API"],"operationId":"getExperienceAvailability","summary":"Get experience availability","description":"Returns availability slots for an experience within the specified date range.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date"},"description":"Start date (YYYY-MM-DD)"},{"name":"to","in":"query","required":true,"schema":{"type":"string","format":"date"},"description":"End date (YYYY-MM-DD)"}],"responses":{"200":{"description":"Availability data"},"400":{"description":"Invalid experience id or date params","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Experience not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["REST API"],"operationId":"updateExperienceAvailability","summary":"Update experience availability","description":"Updates the availability for a specific date: block/unblock the date and override slot counts.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["date"],"properties":{"date":{"type":"string","format":"date"},"blocked":{"type":"boolean"},"blockReason":{"type":"string"},"slotOverrides":{"type":"object"}}}}}},"responses":{"200":{"description":"Availability updated"},"400":{"description":"Invalid experience id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Experience not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/bookings":{"get":{"tags":["REST API"],"operationId":"listBookings","summary":"List bookings","description":"Returns a paginated list of bookings. Supports filtering by status, channel, experience, and property.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"after","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["held","confirmed","cancelled","completed"]}},{"name":"channel","in":"query","required":false,"schema":{"type":"string","enum":["webchat","sms","front_desk","phone","email","agent"]}},{"name":"experience_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"property_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Booking list"},"400":{"description":"Invalid client id or query parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["REST API"],"operationId":"createBooking","summary":"Create a booking","description":"Creates a new booking (hold). Returns 409 if capacity for the requested slot is exhausted.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["experienceId","date","channel"],"properties":{"experienceId":{"type":"string","format":"uuid"},"date":{"type":"string","format":"date"},"slot":{"type":"string"},"variantId":{"type":"string","format":"uuid"},"party":{"type":"array","items":{"type":"object"}},"guestName":{"type":"string"},"guestEmail":{"type":"string","format":"email"},"guestRoom":{"type":"string"},"channel":{"type":"string","enum":["webchat","sms","front_desk","phone","email","agent"]},"specialRequest":{"type":"string"},"idempotencyKey":{"type":"string"}}}}}},"responses":{"201":{"description":"Booking created (held)"},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Capacity exhausted for the requested slot","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/bookings/{id}":{"delete":{"tags":["REST API"],"operationId":"cancelBooking","summary":"Cancel a booking","description":"Cancels a booking. An optional reason may be provided in the request body.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}}}}},"responses":{"204":{"description":"Booking cancelled"},"400":{"description":"Invalid booking id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Booking not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/bookings/{id}/confirm":{"post":{"tags":["REST API"],"operationId":"confirmBooking","summary":"Confirm a held booking","description":"Confirms a booking that is in the 'held' state. Returns 409 if the hold has expired.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Booking confirmed"},"400":{"description":"Invalid booking id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Booking not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Hold has expired; booking must be recreated","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/guests":{"get":{"tags":["REST API"],"operationId":"listGuests","summary":"List guests","description":"Returns a paginated list of guests for the client. Supports search and sorting.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"after","in":"query","required":false,"schema":{"type":"string"}},{"name":"property_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"search","in":"query","required":false,"schema":{"type":"string"}},{"name":"order_by","in":"query","required":false,"schema":{"type":"string","enum":["last_name","created_date"]}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Guest list"},"400":{"description":"Invalid client id or query parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/guests/{id}":{"get":{"tags":["REST API"],"operationId":"getGuest","summary":"Get a guest","description":"Returns the profile of a single guest by UUID.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Guest profile"},"400":{"description":"Invalid guest id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Guest not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/analytics":{"get":{"tags":["REST API"],"operationId":"listAnalyticsRuns","summary":"List analytics runs","description":"Returns a paginated list of analytics run history. Supports filtering by type and status.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"clientId","in":"query","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["CHAT_LOGS","AI_VISIBILITY"]}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["QUEUED","IN_PROGRESS","ERROR","DONE"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"after","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Analytics run list"},"400":{"description":"Invalid query parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/analytics/{analyticsId}/rerun":{"post":{"tags":["REST API"],"operationId":"rerunAnalytics","summary":"Re-run an analytics job","description":"Schedules a re-run of the specified analytics job. Returns 202 Accepted with the new analyticsId. Returns 404 if the original run is not found.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"analyticsId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"clientId":{"type":"string","format":"uuid"}}}}}},"responses":{"202":{"description":"Re-run scheduled","content":{"application/json":{"schema":{"type":"object","properties":{"analyticsId":{"type":"string"},"scheduled":{"type":"boolean"},"failed":{"type":"array","items":{"type":"string"},"description":"Any client IDs that failed to schedule"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Analytics run not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/analytics/{analyticsId}/report":{"get":{"tags":["REST API"],"operationId":"getAnalyticsReport","summary":"Download an analytics report","description":"Streams the analytics report file from S3. Supports html and xlsx formats via the format query parameter. Returns 404 if the run is not found.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"analyticsId","in":"path","required":true,"schema":{"type":"string"}},{"name":"clientId","in":"query","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["html","xlsx"]}}],"responses":{"200":{"description":"Report file stream"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Analytics run or report not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/oauth/rezstream/connect":{"get":{"tags":["REST API"],"operationId":"rezstreamConnect","summary":"Initiate rezStream OAuth connect","description":"Redirects the browser to the rezStream OAuth authorize URL. Requires a valid clientId query parameter and an authenticated session with PMS write permission. Returns 503 if the rezStream integration is not configured.","security":[{"sessionAuth":[]}],"parameters":[{"name":"clientId","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Maison client UUID to connect rezStream for"}],"responses":{"302":{"description":"Redirect to rezStream OAuth authorize URL"},"400":{"description":"Missing or invalid clientId","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"rezStream integration not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/oauth/rezstream/callback":{"get":{"tags":["REST API"],"operationId":"rezstreamCallback","summary":"Handle rezStream OAuth callback","description":"Receives the authorization code and state from rezStream. Validates the HMAC-signed state, exchanges the code for tokens, and saves the integration config. Returns 400 if params are missing or invalid, 503 if not configured.","parameters":[{"name":"code","in":"query","required":true,"schema":{"type":"string"},"description":"Authorization code from rezStream"},{"name":"state","in":"query","required":true,"schema":{"type":"string"},"description":"HMAC-signed state parameter (contains clientId)"}],"responses":{"302":{"description":"Redirect to console on success"},"400":{"description":"Missing or invalid code/state parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"rezStream integration not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/users/{id}/clients":{"get":{"tags":["Users"],"operationId":"getUserClients","summary":"List clients for a user","description":"Returns a paginated list of clients the specified user has access to. Requires authentication.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Client list for the user","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}},"total":{"type":"integer"},"limit":{"type":"integer"},"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}}}}}},"400":{"description":"Invalid user id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/users/{id}/roles":{"get":{"tags":["Users"],"operationId":"getUserRoles","summary":"List roles for a user","description":"Returns all roles assigned to the specified user. Returns 404 if user not found.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"}],"responses":{"200":{"description":"User roles"},"400":{"description":"Invalid user id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/users/{id}/groups":{"get":{"tags":["Users"],"operationId":"getUserGroups","summary":"List groups for a user","description":"Returns all user groups the specified user is a member of. Returns 404 if user not found.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"}],"responses":{"200":{"description":"User groups"},"400":{"description":"Invalid user id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/users/{id}/apikeys":{"get":{"tags":["Users"],"operationId":"getUserApiKeys","summary":"List API keys for a user","description":"Returns all API keys belonging to the specified user. Returns 404 if user not found.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User UUID"}],"responses":{"200":{"description":"User API keys"},"400":{"description":"Invalid user id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/tone":{"get":{"tags":["Legacy"],"operationId":"legacyBcToneGet","summary":"Legacy: Get chatbot tone","description":"Legacy: Returns the chatbot tone prompt string for the client. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Chatbot tone","content":{"application/json":{"schema":{"type":"object","properties":{"chatbotTone":{"type":"string"}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Legacy"],"operationId":"legacyBcToneUpdate","summary":"Legacy: Update chatbot tone","description":"Legacy: Updates the chatbot tone prompt for the client. Empty/whitespace resets to the default tone. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"chatbotTone":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated chatbot tone","content":{"application/json":{"schema":{"type":"object","properties":{"chatbotTone":{"type":"string"}}}}}},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/website/{id}":{"put":{"tags":["Legacy"],"operationId":"legacyBcWebsiteUpdateSingle","summary":"Legacy: Update a website source","description":"Legacy: Updates syncSchedule and/or updateClientWebsite flag for a specific website source by websiteId. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"syncSchedule":{"type":"string","enum":["NONE","DAILY","WEEKLY","BI_WEEKLY","MONTHLY"]},"updateClientWebsite":{"type":"boolean"}}}}}},"responses":{"204":{"description":"Updated"},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Legacy"],"operationId":"legacyBcWebsiteDeleteSingle","summary":"Legacy: Remove a website source","description":"Legacy: Soft-deletes the client_knowledge_source_link for the given websiteId. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Website source removed"},"400":{"description":"Invalid client id or website id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/qa/":{"get":{"tags":["Legacy"],"operationId":"legacyBcQaList","summary":"Legacy: List QA pairs","description":"Legacy: Returns paginated QA pairs. Supports full-text query, id filter, includeDeleted, deletedOnly. Consumer: legacy/qa-editor.","deprecated":true,"x-legacy-consumer":"qa-editor","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"paginationtoken","in":"query","required":false,"schema":{"type":"string"}},{"name":"ids","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated QA pair ids"},{"name":"query","in":"query","required":false,"schema":{"type":"string"},"description":"Full-text search query (mutually exclusive with ids)"},{"name":"includedeleted","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"deletedonly","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"QA pair list","content":{"application/json":{"schema":{"type":"object","properties":{"qa":{"type":"array","items":{"type":"object"}},"paginationToken":{"type":"string","nullable":true}}}}}},"400":{"description":"Invalid client id, query params, or both ids and query provided","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/qa":{"post":{"tags":["Legacy"],"operationId":"legacyBcQaCreate","summary":"Legacy: Create QA pairs","description":"Legacy: Creates one or more QA pairs. Consumer: legacy/qa-editor.","deprecated":true,"x-legacy-consumer":"qa-editor","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"QA pairs created"},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Legacy"],"operationId":"legacyBcQaUpdate","summary":"Legacy: Update QA pairs","description":"Legacy: Updates one or more QA pairs. Consumer: legacy/qa-editor.","deprecated":true,"x-legacy-consumer":"qa-editor","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"QA pairs updated"},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Legacy"],"operationId":"legacyBcQaDelete","summary":"Legacy: Delete QA pairs","description":"Legacy: Soft-deletes one or more QA pairs. Consumer: legacy/qa-editor.","deprecated":true,"x-legacy-consumer":"qa-editor","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"QA pairs deleted"},"400":{"description":"Invalid client id or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/document":{"get":{"tags":["Legacy"],"operationId":"legacyBcDocumentList","summary":"Legacy: List documents","description":"Legacy: Returns documents linked to the client. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Array of documents"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["Legacy"],"operationId":"legacyBcDocumentCreate","summary":"Legacy: Upload a document","description":"Legacy: Uploads a document file (PDF, DOCX, etc.) to S3 and creates a knowledge source link for the client. Accepts multipart/form-data. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"title":{"type":"string"},"language":{"type":"string"}}}}}},"responses":{"201":{"description":"Document uploaded and knowledge link created"},"400":{"description":"Invalid client id or missing file","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"Document storage not provisioned","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/document/{id}":{"get":{"tags":["Legacy"],"operationId":"legacyBcDocumentGet","summary":"Legacy: Get a document","description":"Legacy: Returns the document record and metadata for the specified document id. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document record"},"400":{"description":"Invalid client id or document id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Document not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["Legacy"],"operationId":"legacyBcDocumentUpdate","summary":"Legacy: Update a document","description":"Legacy: Replaces or updates a document file and/or its metadata. Accepts multipart/form-data. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"},"title":{"type":"string"},"language":{"type":"string"}}}}}},"responses":{"200":{"description":"Document updated"},"400":{"description":"Invalid client id or document id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Document not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Legacy"],"operationId":"legacyBcDocumentDelete","summary":"Legacy: Delete a document","description":"Legacy: Soft-deletes the document and its knowledge source link. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Document deleted"},"400":{"description":"Invalid client id or document id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Document not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/document/{id}/sync":{"put":{"tags":["Legacy"],"operationId":"legacyBcDocumentSync","summary":"Legacy: Trigger document sync","description":"Legacy: Triggers reprocessing/re-ingestion of the document into the knowledge base. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Sync queued"},"400":{"description":"Invalid client id or document id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Document not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/document/{id}/versions":{"get":{"tags":["Legacy"],"operationId":"legacyBcDocumentVersions","summary":"Legacy: List document versions","description":"Legacy: Returns the version history for a document from the KB version history table. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document versions"},"400":{"description":"Invalid client id or document id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Document not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/client/asset/list":{"get":{"tags":["Legacy"],"operationId":"legacyBcClientAssetList","summary":"Legacy: List client assets","description":"Legacy: Returns the client asset list. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Asset list"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/client/asset/{id}":{"put":{"tags":["Legacy"],"operationId":"legacyBcClientAssetUpdate","summary":"Legacy: Update client asset metadata","description":"Legacy: Updates the description or published flag of a library asset. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"published":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Asset updated"},"400":{"description":"Invalid client id or asset id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Legacy"],"operationId":"legacyBcClientAssetDeleteById","summary":"Legacy: Delete a client asset by id","description":"Legacy: Deletes a client library asset by id. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Asset deleted"},"400":{"description":"Invalid client id or asset id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/user/removeroles":{"put":{"tags":["Legacy"],"operationId":"legacyBcUserRemoveRoles","summary":"Legacy: Remove roles from user","description":"Legacy: Removes the specified role assignments from a user. Target can be identified by userId or email; omitting both defaults to the current authenticated user. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"roles":{"type":"array","items":{"type":"object","properties":{"roleId":{"type":"string"},"clientId":{"type":"string","format":"uuid"}}}}}}}}},"responses":{"200":{"description":"Roles removed"},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/user/{id}":{"get":{"tags":["Legacy"],"operationId":"legacyBcUserGetById","summary":"Legacy: Get user by id","description":"Legacy: Returns the user record for the specified user id including roles. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"User UUID"}],"responses":{"200":{"description":"User record with roles"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to process request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/pms/services":{"get":{"tags":["Legacy"],"operationId":"legacyBcPmsServices","summary":"Legacy: Get Mews bookable services","description":"Legacy: Fetches Mews bookable services with availability for the client. Requires hotel-config READ permission and the hotel-config feature entitlement. Consumer: legacy/business-console.","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}},{"name":"refresh","in":"query","required":false,"schema":{"type":"string"},"description":"Pass 'true' to bypass cache"},{"name":"startdate","in":"query","required":false,"schema":{"type":"string"}},{"name":"enddate","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"string"},"description":"Max number of services to return (default 20)"}],"responses":{"200":{"description":"Mews services list","content":{"application/json":{"schema":{"type":"object","properties":{"services":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Not Mews provider or token not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"PMS configuration not found for client","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to fetch Mews services","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/legacy/pms/getRatePlans":{"get":{"tags":["Legacy"],"operationId":"legacyBcPmsGetRatePlans","summary":"Legacy: Get Mews rate plans","description":"Legacy: Fetches Mews rate plans for the specified service ids. Requires hotel-config READ permission. Consumer: legacy/business-console. Note: path uses camelCase (legacy convention preserved).","deprecated":true,"x-legacy-consumer":"business-console","security":[{"sessionAuth":[]}],"parameters":[{"name":"x-client-id","in":"header","required":true,"schema":{"type":"string"}},{"name":"serviceids","in":"query","required":true,"schema":{"type":"string"},"description":"Comma-separated Mews service ids"}],"responses":{"200":{"description":"Rate plans for the specified services"},"400":{"description":"Invalid client id or missing serviceids","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"PMS configuration not found for client","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to fetch rate plans","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/tools/sitecheck":{"post":{"tags":["Tools"],"operationId":"siteCheck","summary":"Analyze a website for Maison embed status","description":"Fetches the given URL, detects the framework, and checks whether the Maison chat widget is properly embedded.","security":[{"apiKey":[]},{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteCheckBody"}}}},"responses":{"200":{"description":"Analysis result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteCheckResponse"}}}},"400":{"description":"Invalid URL or private IP blocked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"URL could not be fetched","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/diagnostic":{"post":{"tags":["Diagnostic"],"operationId":"startDiagnostic","summary":"Start a site diagnostic run","description":"Start a diagnostic check of a website. Checks whether Maison is correctly installed on the site and how it is configured, and produces findings with suggested fixes. Returns immediately; progress and results arrive on the real-time channel included in the response and in the session history. If a check is already in progress for the same site, the response indicates it so you can follow along. Checks of the same site share one session and history.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiagnosticStartBody"}}}},"responses":{"202":{"description":"Diagnostic run started (or already running)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiagnosticStartResponse"}}}},"400":{"description":"Invalid URL or malformed request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (5/IP/min)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/diagnostic/{sessionid}":{"get":{"tags":["Diagnostic"],"operationId":"getDiagnosticSession","summary":"Get diagnostic session events","description":"Returns all stored events for the session. An empty events array is valid (session not yet started).","security":[],"parameters":[{"name":"sessionid","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"Opaque session identifier returned by the start-diagnostic response"}}],"responses":{"200":{"description":"Session events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiagnosticSessionResponse"}}}},"400":{"description":"Invalid sessionid format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (30/IP/min)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/catalog":{"get":{"tags":["Integrations"],"operationId":"getIntegrationsCatalog","summary":"List integration provider catalog","description":"Returns available integration providers grouped by integration type (pms, bookingengine, ems, tablemanagement, experiencemanagement, crm). Types with at least one registered provider OR an announced (comingSoon) provider are included. No auth required. Credential values and secrets are never returned.","security":[],"responses":{"200":{"description":"Provider catalog grouped by integration type","content":{"application/json":{"schema":{"type":"object","required":["types"],"properties":{"types":{"type":"array","description":"Integration types that have at least one registered provider","items":{"type":"object","required":["type","label","maxPerClient","providers"],"properties":{"type":{"type":"string","enum":["pms","bookingengine","ems","tablemanagement","experiencemanagement","crm"],"description":"Integration type identifier"},"label":{"type":"string","description":"Human-readable type label"},"maxPerClient":{"type":"integer","nullable":true,"description":"Maximum number of this type allowed per client (null = uncapped)"},"providers":{"type":"array","description":"Providers registered for this type","items":{"type":"object","required":["provider","displayName","type","auth","defaultToolIds","tools"],"properties":{"provider":{"type":"string","description":"Provider slug (e.g. cloudbeds)"},"displayName":{"type":"string","description":"Human-readable name"},"type":{"type":"string","enum":["pms","bookingengine","ems","tablemanagement","experiencemanagement","crm"],"description":"Integration type for this provider"},"comingSoon":{"type":"boolean","description":"True for an announced integration that is not yet implemented — non-connectable, with no auth or tools. Absent for real providers."},"auth":{"type":"object","required":["strategy","credentialFields","configFields"],"properties":{"strategy":{"type":"string","enum":["api_key_header","oauth_client_credentials","oauth_refresh_token","oauth_auth_code","none"],"description":"Authentication strategy for this provider"},"connectVia":{"type":"string","enum":["invitation"],"description":"How this provider is authorised. \"invitation\" means consent can only be granted by the hotel's own account holder, so the console offers a Connect action that emails them a one-time link instead of a credential form. Absent for providers configured by pasting credentials."},"setupSteps":{"type":"array","items":{"type":"string"},"description":"Short ordered setup instructions for this provider, shown to the operator when configuring it. Plain text, no markup — the console escapes them. Absent for providers whose setup is self-evident."},"credentialFields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"secret":{"type":"boolean"}}},"description":"Credential field definitions (labels only — never values)"},"configFields":{"type":"array","items":{"type":"object","required":["key","label"],"properties":{"key":{"type":"string"},"label":{"type":"string"},"required":{"type":"boolean","description":"When true, a missing or blank value is rejected on connect/update (400) and the console blocks submit. Absent means optional. Used for config a provider cannot function without — e.g. StayNTouch's authTokenUrl, whose absence would otherwise surface only as an auth error at the first tool call."},"type":{"type":"string","enum":["text","email"],"description":"Input type hint for the console dialog. Absent means \"text\"."},"discoverVia":{"type":"string","description":"Tool id that discovers valid values for this field. When present the console renders a picker populated through the provider discovery route instead of a free-text input."},"options":{"type":"array","description":"A closed set of permitted values. When present the console renders a dropdown instead of a free-text input. `value` is what is stored and read back by the runtime (a URL, an id — the real value, so no mapping layer is introduced); `label` is what the operator sees. Mutually exclusive with discoverVia.","items":{"type":"object","required":["value","label"],"properties":{"value":{"type":"string"},"label":{"type":"string"}}}}}},"description":"Non-sensitive config field definitions"}}},"defaultToolIds":{"type":"array","items":{"type":"string"},"description":"Tool IDs enabled by default for new connections"},"tools":{"type":"array","items":{"type":"object","required":["id","description","readOnly","requiresVerification","operatorGrade","tags","certified"],"properties":{"id":{"type":"string"},"description":{"type":"string"},"readOnly":{"type":"boolean"},"requiresVerification":{"type":"boolean","description":"When true, the assistant sends a booker-verification code before calling this tool."},"operatorGrade":{"type":"boolean","description":"When true, this tool is staff-facing (operational staff, not guests). autoSplitTemplate routes these to 'client' audience."},"tags":{"type":"array","items":{"type":"string"},"description":"Functional domain tags (closed set: ari, reservations, guests, folios, housekeeping, events, catalog, operations)."},"certified":{"type":"boolean","description":"Whether Maison is certified to use this tool with the vendor. Always present as a boolean (absent in the manifest means NOT certified; the catalog always materialises the resolved boolean, so this field is never absent on the wire). Uncertified tools (false) are forced to 'deny' in every template, excluded from every delivery path regardless of the stored audience map, and refused by the proxy gate with 403. A PUT /api/clients/{id}/integrations/{provider} body that assigns an uncertified tool any audience other than 'deny' is rejected with 400."}}},"description":"All tools provided by this integration"},"templates":{"type":"array","description":"Pre-computed audience templates for this provider. Each template maps every tool id to its recommended audience value. Applying a template is a starting point; operators can fine-tune per-tool afterward.","items":{"type":"object","required":["id","label","description","audiences"],"properties":{"id":{"type":"string","enum":["ari","reservations","autosplit"],"description":"Template identifier"},"label":{"type":"string","description":"Human-readable template label for UI chips"},"description":{"type":"string","description":"Template description (rendered as tooltip text on the chip)"},"audiences":{"type":"object","additionalProperties":{"type":"string","enum":["deny","client","guest","all"]},"description":"Pre-computed audience value per tool id. Covers every tool in the provider manifest."}}}}}}}}}}}}}}},"500":{"description":"Unable to fetch integration catalog","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/integrations":{"get":{"tags":["Integrations"],"operationId":"listClientIntegrations","summary":"List a client's active integrations","description":"Returns the client's active integration rows. Credentials are masked (last 4 chars only) — never decrypted. Pass includeremoved=true to also receive the last 50 soft-deleted (removed) integration rows for audit purposes. Requires session or API-key auth with update-client READ on the client.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"includeremoved","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"When true, the response also includes a 'removed' array containing the last 50 soft-deleted integration rows (most recent first) with createdBy and removedBy audit fields. Credential fields are never returned for removed rows."}],"responses":{"200":{"description":"Active integration list, with optional removed audit trail","content":{"application/json":{"schema":{"type":"object","required":["integrations"],"properties":{"integrations":{"type":"array","items":{"type":"object","required":["id","provider","type","status","createdDate"],"properties":{"id":{"type":"string","format":"uuid"},"provider":{"type":"string","description":"Provider slug"},"type":{"type":"string","description":"Integration type derived from the manifest registry. 'unknown' when the stored provider is no longer registered."},"credentials":{"type":"object","nullable":true,"additionalProperties":{"type":"string"},"description":"Masked credential values (last 4 chars only — never plaintext)"},"config":{"type":"object","additionalProperties":true,"description":"Non-sensitive config"},"enabledToolIds":{"type":"array","items":{"type":"string"},"nullable":true,"description":"Tool IDs enabled for this connection (null = manifest defaults)"},"toolAudiences":{"type":"object","nullable":true,"additionalProperties":{"type":"string","enum":["deny","client","guest","all"]},"description":"Per-tool audience map. Keys are tool IDs; values control which session audience may call the tool: 'guest' (unauthenticated), 'client' (authenticated operator with update-client READ), 'all' (both), or 'deny' (neither). Absent key = 'deny'. Null (legacy row) treats the entire enabled set as guest-only; client gets nothing until configured."},"status":{"type":"string","enum":["PENDING_AUTH","CONNECTED","ERROR","DISCONNECTED"],"description":"Connection lifecycle status"},"tokenExpiresAt":{"type":"string","format":"date-time","nullable":true},"createdDate":{"type":"string","format":"date-time"},"createdBy":{"type":"object","nullable":true,"description":"Operator who connected this integration","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"disabled":{"type":"boolean","description":"True when the integration has been administratively disabled. Disabled integrations deliver zero tools to all audiences and are refused by the proxy gate (403). Config and credentials are preserved; re-enabling restores the prior state."},"disabledDate":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp when the integration was disabled (null if active)"},"disabledBy":{"type":"object","nullable":true,"description":"Operator who last disabled this integration","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}}}}},"removed":{"type":"array","description":"Last 50 soft-deleted integration rows, most recent first. Only present when includeremoved=true.","items":{"type":"object","required":["id","provider","status","createdDate","removedDate"],"properties":{"id":{"type":"string","format":"uuid"},"provider":{"type":"string","description":"Provider slug"},"status":{"type":"string","enum":["PENDING_AUTH","CONNECTED","ERROR","DISCONNECTED"]},"createdDate":{"type":"string","format":"date-time"},"createdBy":{"type":"object","nullable":true,"description":"Operator who connected this integration","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"removedDate":{"type":"string","format":"date-time"},"removedBy":{"type":"object","nullable":true,"description":"Operator who removed this integration","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}}}}}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to list integrations","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/integrations/{provider}":{"put":{"tags":["Integrations"],"operationId":"upsertClientIntegration","summary":"Connect or update a provider integration","description":"Inserts or updates the active integration row for (clientId, provider). Validates the provider against the manifest registry, validates enabledToolIds, encrypts credentials at rest, and projects to DynamoDB. Returns 409 when an active legacy PMS config blocks the connection (pms-type providers only) or when the per-type cap is exceeded on a new connection. Returns 422 when the provider slug is unknown. Requires update-client WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"provider","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9_]+$"},"description":"Provider slug (e.g. cloudbeds)"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"credentials":{"type":"object","nullable":true,"additionalProperties":{"type":"string"},"description":"Raw credential key-value pairs (encrypted before storage)"},"config":{"type":"object","nullable":true,"additionalProperties":true,"description":"Non-sensitive config (propertyId, apiVersion, etc.)"},"enabledToolIds":{"type":"array","items":{"type":"string"},"nullable":true,"description":"Tool IDs to enable (null = use manifest defaults)"},"toolAudiences":{"type":"object","nullable":true,"additionalProperties":{"type":"string","enum":["deny","client","guest","all"]},"description":"Per-tool audience gate. Full-replacement semantics: the entire map is replaced on every PUT (absent key → deny). Pass null to clear the map (reverts to legacy guest-only semantics). All keys must be valid tool IDs for the provider; unknown keys return 400."}}}}}},"responses":{"200":{"description":"Integration upserted","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"Integration row UUID"}}}}}},"400":{"description":"Invalid client id, provider, or request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Conflict — active legacy PMS config blocks pms-type connection, or per-type cap exceeded","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"422":{"description":"Unknown provider slug — not in the manifest registry","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to upsert integration","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["Integrations"],"operationId":"disconnectClientIntegration","summary":"Disconnect a provider integration","description":"Soft-deletes the active integration row for (clientId, provider), wipes all token columns, and removes the DynamoDB projection entry. Returns 404 if no active integration is found. Requires update-client WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"provider","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9_]+$"},"description":"Provider slug"}],"responses":{"204":{"description":"Integration disconnected"},"400":{"description":"Invalid client id or provider","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Integration not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to disconnect integration","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/integrations/{provider}/disable":{"post":{"tags":["Integrations"],"operationId":"disableClientIntegration","summary":"Disable a provider integration","description":"Administratively disables the active integration for (clientId, provider). The integration is immediately excluded from all tool-delivery paths and refused by the proxy gate with 403. Config and credentials are preserved — re-enabling restores the prior state. Operation is idempotent: if already disabled, returns 200 with the current state without changing the original disabled_date or disabled_by. Returns 404 if no active integration exists. Requires update-client WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"provider","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9_]+$"},"description":"Provider slug (e.g. cloudbeds)"}],"responses":{"200":{"description":"Integration disabled (or was already disabled — idempotent)","content":{"application/json":{"schema":{"type":"object","required":["id","provider","status","disabled"],"properties":{"id":{"type":"string","format":"uuid"},"provider":{"type":"string"},"status":{"type":"string","enum":["PENDING_AUTH","CONNECTED","ERROR","DISCONNECTED"]},"disabled":{"type":"boolean","description":"Always true after this call"},"disabledDate":{"type":"string","format":"date-time","nullable":true},"disabledBy":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}}}}}}},"400":{"description":"Invalid client id or provider","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Integration not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to disable integration","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/integrations/resnexus/properties":{"get":{"tags":["Integrations"],"operationId":"listResnexusBusinesses","summary":"List the ResNexus businesses the client can be bound to","description":"Runs resnexus_get_businesses against the Availability Exchange gateway with Maison's Integration Partner account and returns every ResNexus property that has added the Maison channel — the whole partner account, not just this client — so the console can offer a picker for the row's businessId (MA-853). Response shape matches the rezStream and Mews discovery routes ({properties:[{id,name,address,currency,timezone,bookablePeriod}]}); timezone and bookablePeriod are always null because businesses carry neither. Persists businessName next to the selected businessId and clears it when that business is no longer listed. Returns 409 when the integration row has not been saved yet (a workflow state, not a fault) and 502 when the gateway call fails, including a missing partner credential.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The ResNexus businesses on Maison's partner account","content":{"application/json":{"schema":{"type":"object","properties":{"properties":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","description":"ResNexus business_id, as a string"},"name":{"type":"string","description":"Business name; falls back to the id when the vendor returns none"},"address":{"type":["string","null"],"description":"Full postal address when the vendor returns one"},"currency":{"type":["string","null"],"description":"Three-letter currency code"},"timezone":{"type":["string","null"],"description":"Always null — shape parity with rezStream"},"bookablePeriod":{"type":["object","null"],"description":"Always null — shape parity with rezStream"}}}}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Integration row not saved yet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"Availability Exchange call failed","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/integrations/mews/properties":{"get":{"tags":["Integrations"],"operationId":"listMewsServices","summary":"List the client's Mews services","description":"Returns the enterprise's active bookable services from the client's Mews account. Two filter layers are applied before the calendar lookup: (1) IsActive=true and Discriminator=Bookable — non-bookable add-ons (Discriminator=Additional: spa, restaurant, tour packages) are excluded; (2) Data.Value.TimeUnitPeriod != Hour — hour-unit bookable services (parking, restaurant, tours) are excluded because they reject a 365-day calendar window with 400 and are not the target of the accommodation serviceId picker. Absence of TimeUnitPeriod is treated as Day/Month and those services remain. For each returned service the handler also queries mews_get_service_calendar with a 365-day window to recover the first and last night Mews will actually sell; this is a queried window rather than a vendor-defined validity period because Mews does not clip the window to service validity (the window gives accurate nights-available, not a metadata field). Response shape matches the rezStream discovery route ({properties:[{id,name,bookablePeriod,...}]}) so the console stays provider-agnostic; timezone, currency and address are always null because Mews services carry none. Returns 409 when the integration row is absent or has no saved access token — both are workflow states, not faults.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The enterprise's Mews services","content":{"application/json":{"schema":{"type":"object","properties":{"properties":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","description":"Mews service id (GUID)"},"name":{"type":"string","description":"Service name; falls back to the id when Mews returns none"},"timezone":{"type":["string","null"],"description":"Always null for Mews — shape parity with rezStream"},"currency":{"type":["string","null"],"description":"Always null for Mews — shape parity with rezStream"},"address":{"type":["string","null"],"description":"Always null for Mews — shape parity with rezStream"},"bookablePeriod":{"type":["object","null"],"description":"The first and last night Mews will sell for this service in the next 365 days (property-local YYYY-MM-DD dates). Queried live via mews_get_service_calendar — this is a window of actual inventory, not a vendor-defined validity period. null when the service returned no nights or the calendar lookup failed (HOUR-unit services, no inventory in window, or rate limit).","properties":{"startDate":{"type":"string","format":"date","description":"YYYY-MM-DD, property-local; first night Mews will sell in the next 365 days"},"endDate":{"type":"string","format":"date","description":"YYYY-MM-DD, property-local; last night Mews will sell in the next 365 days"}},"required":["startDate","endDate"]}}}}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Integration not saved, or no access token stored yet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to list Mews services","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"Could not reach Mews","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/integrations/rezstream/properties":{"get":{"tags":["Integrations"],"operationId":"listRezstreamProperties","summary":"List the client's rezStream properties","description":"Returns the properties on the client's rezStream account, so the business console can offer a picker instead of asking an operator to type a Property ID. Resolves the client's stored rezStream credentials server-side and calls the provider's own rezstream_get_inventory tool through the integration proxy — no credential is exposed to the browser. When the account returns no property rows but has business-level data, synthesises a single entry from the business object so the picker is never blank. Returns 409 when the rezStream credentials have not been saved yet (a workflow step, not a fault), and 502 when rezStream is unreachable or refuses. Requires update-client READ.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Properties for this rezStream account","content":{"application/json":{"schema":{"type":"object","required":["properties"],"properties":{"properties":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","description":"rezStream property id — stored as propertyId and injected into every rezStream tool call"},"name":{"type":"string","description":"Property name shown in the console"},"timezone":{"type":["string","null"],"description":"IANA timezone of the property"},"currency":{"type":["string","null"],"description":"ISO 4217 currency code"},"address":{"type":["string","null"],"description":"Street address, when set"}}}}}}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"rezStream credentials have not been saved for this client yet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to list rezStream properties","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"rezStream was unreachable or refused the request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/integrations/{provider}/configcheck":{"get":{"tags":["Integrations"],"operationId":"checkIntegrationConfigValue","summary":"Validate one integration config value against the provider","description":"Resolves a single operator-entered configuration value against the provider's own API, so a value that could never work is rejected while the operator is still in the console rather than silently failing in front of a guest. The value is sent to the vendor as an opaque STRING and is never coerced (STS Cloud's RFP form lookup accepts either the numeric id or the permalink slug). Beyond confirming the value resolves, the response lists every field the resolved record marks REQUIRED and, separately, which of those the tool named by collectedBy does not collect — that second list is the operator's warning that calls will still be rejected even though the id itself is correct. The vendor call runs through the integration proxy using the client's stored credentials, so no credential reaches the browser. Checks are registered per (provider, config field); today the only one is stscloud.rfpFormId. Returns 404 when the value does not resolve at the provider, 409 when the integration is not saved or not connected, and 502 when the provider is unreachable — so an operator can tell a mistyped value from an outage. Requires update-client READ.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"provider","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9_]+$"},"description":"Provider slug (e.g. stscloud)"},{"name":"field","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64},"description":"Key of the provider config field being checked, as declared in the provider manifest (e.g. rfpFormId)"},{"name":"value","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":256},"description":"The value to validate. Trimmed, then passed to the provider verbatim as a string — never coerced to a number"}],"responses":{"200":{"description":"The value resolves at the provider","content":{"application/json":{"schema":{"type":"object","required":["provider","field","value","resolvedId","name","collectedBy","requiredFields","uncollectedRequiredFields"],"properties":{"provider":{"type":"string","description":"Provider slug that was checked"},"field":{"type":"string","description":"Config field key that was checked"},"value":{"type":"string","description":"The trimmed value that was checked"},"resolvedId":{"type":["string","null"],"description":"The provider's canonical id for the resolved record, when it returns one. Lets the console normalise a permalink slug to the numeric id."},"name":{"type":["string","null"],"description":"Display name of the resolved record, when the provider returns one"},"collectedBy":{"type":"string","description":"Id of the Maison tool whose declared parameters were compared against the required-field list"},"requiredFields":{"type":"array","description":"Every field the resolved record marks required","items":{"type":"object","description":"One field of the resolved provider record. `name` is the provider's machine name, or null when the provider sent only a human label — a nameless field cannot be matched against a tool's parameters and is therefore always reported as uncollected.","required":["name","label"],"properties":{"name":{"type":["string","null"],"description":"Provider machine name for the field, when it sends one"},"label":{"type":"string","description":"Human-readable label, falling back to the machine name"}}}},"uncollectedRequiredFields":{"type":"array","description":"Subset of requiredFields the collectedBy tool does not send. Non-empty means every call will be rejected by the provider even though the value itself is valid.","items":{"type":"object","description":"One field of the resolved provider record. `name` is the provider's machine name, or null when the provider sent only a human label — a nameless field cannot be matched against a tool's parameters and is therefore always reported as uncollected.","required":["name","label"],"properties":{"name":{"type":["string","null"],"description":"Provider machine name for the field, when it sends one"},"label":{"type":"string","description":"Human-readable label, falling back to the machine name"}}}}}}}}},"400":{"description":"Invalid client id, invalid provider slug, or missing field/value query parameter","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Unknown provider, no check registered for that provider and field, or the value does not resolve at the provider","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"The integration has not been saved, has never been connected, or the provider refused Maison's authorization","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to check the integration config value","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"The provider was unreachable, errored, or returned an unusable response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/integrations/{provider}/enable":{"post":{"tags":["Integrations"],"operationId":"enableClientIntegration","summary":"Re-enable a provider integration","description":"Clears the administrative disable on the active integration for (clientId, provider), immediately restoring tool delivery for all configured audiences. Operation is idempotent: if already enabled, returns 200 with the current state. Returns 404 if no active integration exists. Requires update-client WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"},{"name":"provider","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9_]+$"},"description":"Provider slug (e.g. cloudbeds)"}],"responses":{"200":{"description":"Integration enabled (or was already enabled — idempotent)","content":{"application/json":{"schema":{"type":"object","required":["id","provider","status","disabled"],"properties":{"id":{"type":"string","format":"uuid"},"provider":{"type":"string"},"status":{"type":"string","enum":["PENDING_AUTH","CONNECTED","ERROR","DISCONNECTED"]},"disabled":{"type":"boolean","description":"Always false after this call"},"disabledDate":{"type":"string","format":"date-time","nullable":true,"description":"Always null after re-enable"},"disabledBy":{"type":"object","nullable":true,"description":"Always null after re-enable","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}}}}}}},"400":{"description":"Invalid client id or provider","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Integration not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Unable to enable integration","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/hubspot/companies":{"get":{"tags":["HubSpot"],"operationId":"searchHubspotCompanies","summary":"Search Maison's HubSpot companies","description":"Full-text search (name, domain, website, phone) over the Company records in MAISON'S OWN HubSpot portal, used by the business console to link a client to the company that represents it (MA-869). This is a Maison-internal capability, not a hotel integration: the hotel never sees or connects it, and the results belong to Maison's CRM. Returns at most 10 matches sorted by name. Requires hubspot READ (Maison admins hold it through ALL:EXECUTE). Returns 502 when HubSpot rejects the request, including when Maison's private-app token is invalid.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":100},"description":"Search text; trimmed. Under 2 characters is rejected."}],"responses":{"200":{"description":"Matching companies. `items` holds the results (up to 10). `query` is the trimmed input. When the original query returned zero results and contained 2+ words, the longest word (≥2 chars) is tried and, if it returns results, that word is set in `fallbackQuery`; otherwise `fallbackQuery` is null.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubspotCompanySearchResult"}}}},"400":{"description":"q missing or outside 2–100 characters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Missing hubspot READ","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"HubSpot returned an error (5xx, rate limit, malformed body)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"HubSpot is not configured for this environment (HUBSPOT_* configuration missing, or HubSpot rejected Maison's access token)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/hubspotcompany":{"get":{"tags":["HubSpot"],"operationId":"getHubspotCompanyMapping","summary":"Get a client's HubSpot company mapping","description":"Returns the ONE HubSpot company (in Maison's own portal) this client is linked to, with the outcome of the last weekly chat_volume_weekly push, plus `suggestedQuery` — the client's name, which the console pre-fills the company search with (HubSpot's search matches name and domain alike). `mapping` is null when the client is not linked. Requires hubspot READ.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"200":{"description":"Mapping state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubspotCompanyMappingResponse"}}}},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Missing hubspot READ","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["HubSpot"],"operationId":"setHubspotCompanyMapping","summary":"Link a client to a HubSpot company","description":"Links the client to exactly one Company in Maison's own HubSpot portal, replacing any previous link (the old row is soft-deleted and kept as history). Mapping is 1-to-1 in both directions: 409 when the company is already linked to a different client (mappedClientId present), or when a concurrent-save race is detected (409 without mappedClientId). A re-link also re-enables a mapping that the weekly job disabled after HubSpot reported the company gone. Requires hubspot WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["hubspotCompanyId"],"properties":{"hubspotCompanyId":{"type":"string","pattern":"^\\d{1,32}$","description":"HubSpot company record id (numeric string)"},"hubspotCompanyName":{"type":["string","null"],"maxLength":256,"description":"Display name captured at link time"},"hubspotCompanyDomain":{"type":["string","null"],"maxLength":256,"description":"HubSpot `domain` property captured at link time"}}}}}},"responses":{"200":{"description":"Updated mapping state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubspotCompanyMappingResponse"}}}},"400":{"description":"Invalid client id, or invalid body. A body failure carries the Zod issues in `details` so the console can point at the offending field.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"details":{"type":"array","description":"Zod issues (present only for body validation failures)","items":{"type":"object","properties":{"path":{"type":"array","items":{"type":"string"}},"message":{"type":"string"},"code":{"type":"string"}}}}}}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Missing hubspot WRITE","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Company already linked to another client, or concurrent-save race detected","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"mappedClientId":{"type":["string","null"]}}}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"HubSpot is not configured for this environment (HUBSPOT_* configuration missing, or HubSpot rejected Maison's access token)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"delete":{"tags":["HubSpot"],"operationId":"removeHubspotCompanyMapping","summary":"Unlink a client from its HubSpot company","description":"Soft-deletes the active mapping. The weekly job stops pushing for this client; the HubSpot property is left at its last value. Requires hubspot WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"responses":{"204":{"description":"Unlinked"},"400":{"description":"Invalid client id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Missing hubspot WRITE","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"No active mapping for this client","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/hubspotcompany/push":{"post":{"tags":["HubSpot"],"operationId":"pushHubspotChatVolume","summary":"Push last week's chat volume for one client to HubSpot","description":"Pushes the last closed UTC week's chat volume to the client's linked HubSpot company. Acquires the same advisory lock as the weekly run (key 869001); returns 409 with reason='busy' if the batch job is running. Returns 409 with reason='unmapped' if the client has no active link. Use dryRun=true to count chats without writing. Demo clients ARE pushed by this route although the Sunday job skips them; disabled clients are skipped. Requires hubspot WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"dryRun":{"type":"boolean","description":"If true, count chats without writing to HubSpot"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Push result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubspotClientPushResult"}}}},"400":{"description":"Invalid client id, or invalid body. A body failure carries the Zod issues in `details` so the console can point at the offending field.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"details":{"type":"array","description":"Zod issues (present only for body validation failures)","items":{"type":"object","properties":{"path":{"type":"array","items":{"type":"string"}},"message":{"type":"string"},"code":{"type":"string"}}}}}}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Missing hubspot WRITE","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Conflict — client unmapped or sync busy","content":{"application/json":{"schema":{"type":"object","required":["error","reason"],"properties":{"error":{"type":"string"},"reason":{"type":"string","enum":["unmapped","busy"]}}}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"HubSpot upstream failure","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"HubSpot not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/clients/{id}/hubspotcompany/create":{"post":{"tags":["HubSpot"],"operationId":"createHubspotCompany","summary":"Create a HubSpot company and link it to a client","description":"Creates a new Company record in Maison's OWN HubSpot portal (Maison-internal), then links it to the client (replacing any previous link); see 409 for the partial-failure case. Use when a search returns no match. Requires hubspot WRITE.","security":[{"sessionAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Maison client id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":256,"description":"Company name"},"domain":{"type":["string","null"],"maxLength":256,"description":"Company domain (e.g. hotel.com); empty string becomes null"}}}}}},"responses":{"201":{"description":"Company created and linked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubspotCompanyCreateResult"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Missing hubspot WRITE","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"409":{"description":"Company was created in HubSpot but could not be linked to the client. `reason` is always `\"created_unlinked\"`. Search for the company and link it manually.","content":{"application/json":{"schema":{"type":"object","required":["error","reason","company"],"properties":{"error":{"type":"string"},"reason":{"type":"string","enum":["created_unlinked"]},"company":{"$ref":"#/components/schemas/HubspotCompany"}}}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"HubSpot upstream failure","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"503":{"description":"HubSpot not configured","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/diagnostic/{sessionid}/chat":{"post":{"tags":["Diagnostic"],"operationId":"diagnosticChat","summary":"Send a follow-up chat message to the diagnostic agent","description":"Send a follow-up question about the diagnostic findings for a site. Returns the agent's reply. The sessionId must match an existing diagnostic session.","security":[],"parameters":[{"name":"sessionid","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"Opaque session identifier returned by the start-diagnostic response"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiagnosticChatBody"}}}},"responses":{"200":{"description":"Chat assistant reply","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiagnosticChatResponse"}}}},"400":{"description":"Invalid sessionid or malformed request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No diagnostic session found for this sessionid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (10/IP/min)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The analysis service was unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"TextPart":{"type":"object","required":["type","text"],"properties":{"type":{"type":"string","enum":["text"]},"text":{"type":"string"}}},"Message":{"type":"object","required":["role","parts"],"properties":{"role":{"type":"string","enum":["user","agent"]},"parts":{"type":"array","items":{"$ref":"#/components/schemas/TextPart"},"minItems":1}}},"Artifact":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"parts":{"type":"array","items":{"$ref":"#/components/schemas/TextPart"},"minItems":1},"index":{"type":"integer","minimum":0,"default":0}},"required":["parts"]},"TaskStatus":{"type":"object","required":["state","timestamp"],"properties":{"state":{"type":"string","enum":["submitted","working","completed","failed","canceled"]},"message":{"$ref":"#/components/schemas/Message"},"timestamp":{"type":"string"}}},"Task":{"type":"object","required":["id","clientId","status"],"properties":{"id":{"type":"string"},"clientId":{"type":"string"},"status":{"$ref":"#/components/schemas/TaskStatus"},"artifacts":{"type":"array","items":{"$ref":"#/components/schemas/Artifact"}},"history":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"metadata":{"type":"object","additionalProperties":true},"expiresAt":{"type":"integer"}}},"JsonRpcRequest":{"type":"object","required":["jsonrpc","id","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"oneOf":[{"type":"string"},{"type":"number"}]},"method":{"type":"string"},"params":{"type":"object","additionalProperties":true}}},"JsonRpcResponse":{"type":"object","required":["jsonrpc","id"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"oneOf":[{"type":"string"},{"type":"number"}]},"result":{},"error":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{}},"required":["code","message"]}}},"ChatMessage":{"type":"object","required":["role","message","datetime"],"properties":{"role":{"type":"string","enum":["assistant","user","system"]},"message":{"type":"string"},"datetime":{"oneOf":[{"type":"string"},{"type":"number"}]},"isOpeningMessage":{"type":"boolean"},"isAdmin":{"type":"boolean"},"context":{"type":"array","items":{"type":"string"}},"type":{"type":"string"},"locale":{"type":"string"},"selection":{"type":"string","description":"PROPERTY_SELECTION envelopes only (multi-property routing): the guest's stated property choice as free text, which the widget POSTs to /api/clients/{id}/handoff."},"pendingRequest":{"type":"string","description":"Guest's pending request carried on a PROPERTY_SELECTION reply."},"isHandoffGreeting":{"type":"boolean","description":"True only on the assistant greeting the multi-property handoff seeds into a new session. A structural marker rather than a prose match: the next handoff's carry filter drops it so a chained handoff never shows two greetings, and the chat widget uses it to recognise a handed-off session whose dispatched first turn is still unanswered. Absent on every other message."},"handoffKey":{"type":"string","description":"Provider message id of the inbound turn that produced this handoff greeting; server-side idempotency marker. Present only alongside isHandoffGreeting on a channel (DM) handoff, where the child session is seeded after the greeting was delivered and a retry of the same inbound message must not seed it twice. Absent on web handoffs and on every other message."},"isTestSession":{"type":"boolean"},"generated":{"type":"boolean","description":"True when the message text was supplied by the host-page automation API (`mcbs.sendMessage`) rather than typed by the guest or produced by the LLM. Set on user messages when the query came from automation, and always set on assistant messages when `injectedResponse` bypassed the LLM turn."},"translated":{"type":"boolean","description":"True when the message text was machine-translated from a different locale before being stored. Allows chat logs to distinguish the original wording from a translated rendition."}}},"QueryBody":{"type":"object","required":["id","query","messages"],"properties":{"id":{"type":"string","description":"Session ID"},"query":{"type":"string","description":"User's query text"},"localTime":{"type":"string"},"messages":{"type":"array","items":{"$ref":"#/components/schemas/ChatMessage"}},"pmsConfig":{"type":"object","nullable":true,"properties":{"pmsProvider":{"type":"string"},"propertyConfig":{"type":"array","items":{"type":"object"}},"serviceConfig":{"type":"array","items":{"type":"object"}},"rateConfig":{"type":"array","items":{"type":"object"}}}},"messageToken":{"type":"string"},"isAuto":{"type":"boolean","description":"True when this turn was posted by the host-page automation API (`mcbs.sendMessage`) rather than typed by the guest. Recorded on the user message so chat logs can distinguish automated from typed turns."},"pageUrl":{"type":"string","description":"URL of the page the message was sent from. http/https only, capped at 2000 chars. Answers which page the guest triggered this from — most useful for automated messages where the triggering page is the whole context."},"generated":{"type":"boolean","description":"Marks the user message as automation-supplied (not guest-typed). Persisted for finer-grained analytics."},"translated":{"type":"boolean","description":"Marks the user message text as machine-translated from another locale before being sent."},"injectedResponse":{"type":"object","description":"When present, the LLM turn is skipped entirely. The supplied text is published as the assistant message immediately. The assistant message always carries `generated: true`.","required":["text"],"properties":{"text":{"type":"string","minLength":1,"description":"The pre-known assistant reply to publish."},"locale":{"type":"string","description":"Locale of the injected text."},"translated":{"type":"boolean","description":"True when the injected text was machine-translated. Stored on the assistant message."}}}}},"ApiKeyCreateBody":{"type":"object","properties":{"description":{"type":"string","default":"API key"}}},"ApiKeyCreateResponse":{"type":"object","required":["key","masked","description","userId"],"properties":{"key":{"type":"string","description":"Full API key — shown once, store securely"},"masked":{"type":"string","description":"Masked key for display"},"description":{"type":"string"},"userId":{"type":"string"}}},"McpQueryHotelParams":{"type":"object","required":["clientId","query"],"properties":{"clientId":{"type":"string","description":"Client/tenant ID"},"query":{"type":"string","description":"The question to ask"},"sessionId":{"type":"string","description":"Existing session ID to continue a conversation"},"locale":{"type":"string","description":"Locale code (e.g. en, fr, de)"}}},"McpGetSessionParams":{"type":"object","required":["clientId","sessionId"],"properties":{"clientId":{"type":"string","description":"Client/tenant ID"},"sessionId":{"type":"string","description":"Session ID to retrieve"}}},"McpCreateSessionParams":{"type":"object","required":["clientId"],"properties":{"clientId":{"type":"string","description":"Client/tenant ID"},"locale":{"type":"string","description":"Locale code"}}},"McpGetClientInfoParams":{"type":"object","required":["clientId","fields"],"properties":{"clientId":{"type":"string","description":"Client/tenant ID"},"fields":{"type":"string","description":"Comma-separated field paths (e.g. hotelConfig.name,clientAssets.icon)"},"locale":{"type":"string","description":"Locale code for translation fields"}}},"McpListTranslationsParams":{"type":"object","required":["clientId","keys"],"properties":{"clientId":{"type":"string","description":"Client/tenant ID"},"keys":{"type":"string","description":"Comma-separated translation key names, or '*' for all"},"locales":{"type":"string","description":"Comma-separated locale codes, or '*' for all"}}},"McpGuestSearchHotelsParams":{"type":"object","properties":{"query":{"type":"string","description":"Optional free-text filter matched against hotel name and address. Omit to list every enabled hotel (guestapp-entitled and opted in)."}}},"McpGuestGetHotelInfoParams":{"type":"object","required":["client_id"],"properties":{"client_id":{"type":"string","description":"Hotel ID as returned by search_hotels. Hotels not enabled for the guest app (guestapp entitlement + CHATGPT opt-in) are not addressable."}}},"McpGuestAskHotelParams":{"type":"object","required":["client_id","message"],"properties":{"client_id":{"type":"string","description":"Hotel ID as returned by search_hotels. Hotels not enabled for the guest app (guestapp entitlement + CHATGPT opt-in) are not addressable."},"message":{"type":"string","description":"The guest's message, in the guest's own words."},"session_token":{"type":"string","description":"Opaque continuation token returned as `sessionToken` by a previous ask_hotel result. Echo it back verbatim to continue that conversation thread. Omitting it does NOT start a new conversation — the hotel continues the thread this connection is already having with it; only an expired or unrecognised token starts a fresh thread, and it never reaches another guest's conversation. The token is sealed by the server (AES-256-GCM, bound to the hotel it was issued for) — clients must not construct, parse, modify, or derive one, and must not treat its contents as meaningful. A conversation lasts at most 24 hours from its first message; after that the hotel starts a fresh thread and no longer has the earlier turns."}}},"McpGuestToolResult":{"type":"object","required":["content"],"properties":{"content":{"type":"array","description":"Always present, on success and on refusal alike. For the two read tools a success carries two text blocks: one short human sentence the model relays, then `structuredContent` serialized as JSON for hosts that do not read `structuredContent` (MCP 2025-06-18); a refusal carries the sentence alone. For `ask_hotel` it is a JSON object carrying `reply`, `sessionToken`, and sometimes `type` and `actionTaken`.","items":{"type":"object","required":["type","text"],"properties":{"type":{"type":"string","enum":["text"]},"text":{"type":"string"}}}},"structuredContent":{"type":"object","description":"The model-visible data channel, read verbatim by the model, and the payload a widget template is hydrated from. Its shape is DECLARED, not merely conventional: `search_hotels` publishes an `outputSchema` of `{ hotels: [{ clientId, name }], count }` and `get_hotel_info` one of `{ clientId, name, website?, address? }`, both readable on `tools/list`. That declaration is half of the widget wiring — the tool `_meta` says which template to render, the `outputSchema` says what it will be filled with, and a host told only the former has nothing to hydrate. `ask_hotel` declares no `outputSchema` and sends no `structuredContent`: its answer is prose, and a declared schema there would make the SDK reject every successful turn. Refusals send none either. Each hotel's `clientId` stays in this channel on purpose: it is the only addressable hotel id, so a client that renders no widget can still drive the whole conversation from here."},"_meta":{"type":"object","description":"The widget-only channel: delivered to the MCP-Apps widget and hidden from the model, so nothing carried here costs model context. `search_hotels` sends `{ hotels: [{ clientId, name, website?, address? }], count }`; `get_hotel_info` sends `{ hotel: { clientId, name, website?, address?, heroImageUrl?, iconImageUrl? } }`. The image URLs ride this channel precisely because they are bulk the model cannot act on."},"isError":{"type":"boolean","description":"`true` on a refusal — a hotel this assistant cannot serve, a ceiling reached, or a turn that failed. A refusal carries `content` only: no `structuredContent`, no `_meta`, and so no card to render."}}},"McpGuestWidgetTemplate":{"type":"object","required":["uri","mimeType","text"],"properties":{"uri":{"type":"string","enum":["ui://maison/guest/hotellist/v2.html","ui://maison/guest/hotelcard/v2.html"],"description":"The versioned template URI, and the host's cache key — hosts cache aggressively on it, so a breaking change to a fragment ships as a new version segment (`/v1.html` to `/v2.html`), never as an edit under the same URI. `hotellist` backs `search_hotels`; `hotelcard` backs `get_hotel_info`."},"mimeType":{"type":"string","enum":["text/html;profile=mcp-app"],"description":"The MCP-Apps profile media type, exactly — no space after the semicolon. A host that does not match it renders the fragment as plain text."},"text":{"type":"string","description":"The widget: a self-contained HTML fragment (no doctype, no `<html>` wrapper) with its CSS and JS inlined. The host renders it in an isolated iframe under `default-src 'none'`, so it fetches nothing — every byte it displays arrived in the tool result."},"_meta":{"type":"object","description":"Render hints the host reads from the resource rather than the tool: `ui.prefersBorder`, and the CSP allowlist `ui.csp` (mirrored as the legacy `openai/widgetCSP`). The only allowed resource origin is the origin of this deployment's site host, where the `/da/hero/{id}` and `/da/icon/{id}` redirects live; the connect list is empty because the widget fetches nothing. Exact origins only — the host supports no wildcards."}}},"McpListClientsParams":{"type":"object","properties":{"search":{"type":"string","description":"Filter by name, description, or website domain"},"limit":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Results per page"},"after":{"type":"string","description":"Cursor for next page"},"order_by":{"type":"string","enum":["name","created_date","website_id","demo_client","last_scraped_date","ai_visibility_schedule"],"default":"created_date","description":"Sort column"},"sort":{"type":"string","enum":["ASC","DESC"],"default":"DESC","description":"Sort direction"}}},"McpGetClientParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Client ID (UUID)"}}},"McpCreateClientParams":{"type":"object","required":["name","website_id"],"properties":{"name":{"type":"string","description":"Client display name"},"website_id":{"type":"string","format":"uuid","description":"Associated website UUID"},"demo_client":{"type":"boolean","default":false,"description":"Whether this is a demo client"},"description":{"type":"string","nullable":true,"default":null,"description":"Optional description"},"scrape_language":{"type":"string","nullable":true,"default":null,"description":"Language code for scraping"},"chat_logs_analytics_schedule":{"type":"string","enum":["NONE","DAILY","WEEKLY","BI_WEEKLY","MONTHLY"],"default":"NONE","description":"Analytics schedule"},"ai_visibility_schedule":{"type":"string","enum":["NONE","DAILY","WEEKLY","BI_WEEKLY","MONTHLY"],"default":"NONE","description":"AI visibility schedule"},"manual_knowledge":{"type":"boolean","default":false,"description":"When true the KB is curated manually; automated website-sync is skipped and on-demand scrape triggers refuse unless force=true is passed. MA-1158."},"multi_property":{"type":"boolean","default":false,"description":"When true this client's website is a multi-property group/brand site: property DISCOVERY runs instead of a whole-site scrape, and the knowledge base is partitioned across the child clients discovery proposes. Setting it also FORCES manual_knowledge — a group site must never be scraped whole into the parent's own namespace, the decision has to be made in the same call that creates the client (a client only becomes a parent afterwards, via /:id/children), and there is no undo that clears an already-ingested KB. MA-1218."}}},"McpUpdateClientParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Client ID (UUID) to update"},"name":{"type":"string","description":"New display name"},"website_id":{"type":"string","format":"uuid","description":"New website UUID"},"demo_client":{"type":"boolean","description":"Set demo flag"},"description":{"type":"string","nullable":true,"description":"New description"},"scrape_language":{"type":"string","nullable":true,"description":"New scrape language"},"chat_logs_analytics_schedule":{"type":"string","enum":["NONE","DAILY","WEEKLY","BI_WEEKLY","MONTHLY"],"description":"New analytics schedule"},"ai_visibility_schedule":{"type":"string","enum":["NONE","DAILY","WEEKLY","BI_WEEKLY","MONTHLY"],"description":"New AI visibility schedule"}}},"McpDisableClientParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Client ID (UUID) to disable"}}},"McpListUsersParams":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Results per page"},"after":{"type":"string","description":"Cursor for next page"},"search":{"type":"string","description":"Filter by name or email"},"status":{"type":"string","enum":["active","disabled","invited","expired"],"description":"Filter by status"},"order_by":{"type":"string","enum":["email","last_name","status","last_login_date","created_date"],"default":"created_date","description":"Sort column"},"sort":{"type":"string","enum":["ASC","DESC"],"default":"DESC","description":"Sort direction"}}},"McpGetUserParams":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string","description":"User ID (UUID)"}}},"McpInviteUserParams":{"type":"object","required":["email","first_name","last_name"],"properties":{"email":{"type":"string","format":"email","description":"Email address"},"first_name":{"type":"string","description":"First name"},"last_name":{"type":"string","description":"Last name"},"invite_expiry_days":{"type":"integer","minimum":1,"maximum":90,"default":7,"description":"Days until invite expires"}}},"McpUpdateUserParams":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string","description":"User ID (UUID) to update"},"first_name":{"type":"string","description":"New first name"},"last_name":{"type":"string","description":"New last name"},"phone":{"type":"string","nullable":true,"description":"Phone number (null to clear)"},"enable":{"type":"boolean","description":"Set true to re-enable a disabled user"}}},"McpDisableUserParams":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string","description":"User ID (UUID) to disable"}}},"McpReinviteUserParams":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string","description":"User ID (UUID) to reinvite"},"invite_expiry_days":{"type":"integer","minimum":1,"maximum":90,"default":7,"description":"Days until new invite expires"}}},"McpForceResetUserParams":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string","description":"User ID (UUID) to force reset"}}},"McpGetUserActivityParams":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string","description":"User ID (UUID)"},"limit":{"type":"integer","minimum":1,"maximum":100,"default":50,"description":"Events per page"},"after":{"type":"string","description":"Cursor for next page"}}},"McpListRolesParams":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Results per page"},"after":{"type":"string","description":"Cursor for next page"},"search":{"type":"string","description":"Filter by name or description"},"order_by":{"type":"string","enum":["name","created_date"],"default":"created_date","description":"Sort column"},"sort":{"type":"string","enum":["ASC","DESC"],"default":"DESC","description":"Sort direction"}}},"McpGetRoleParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Role ID (UUID)"}}},"McpCreateRoleParams":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Role name"},"description":{"type":"string","description":"Optional description"},"client_id":{"type":"string","format":"uuid","description":"Client ID to scope role"},"visibility_maison_only":{"type":"boolean","description":"Only visible to Maison admins"}}},"McpUpdateRoleParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Role ID (UUID) to update"},"name":{"type":"string","description":"New role name"},"description":{"type":"string","description":"New description"},"visibility_maison_only":{"type":"boolean","description":"Only visible to Maison admins"}}},"McpDisableRoleParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Role ID (UUID) to disable"}}},"McpAddRolePolicyParams":{"type":"object","required":["role_id","permission"],"properties":{"role_id":{"type":"string","description":"Role ID (UUID)"},"feature_id":{"type":"string","format":"uuid","description":"Feature ID restriction"},"client_id":{"type":"string","format":"uuid","description":"Client ID restriction"},"client_region_id":{"type":"string","format":"uuid","description":"Client region ID restriction"},"client_department_id":{"type":"string","format":"uuid","description":"Client department ID restriction"},"tag_id":{"type":"string","format":"uuid","description":"Tag ID restriction"},"permission":{"type":"string","enum":["READ","WRITE","EXECUTE","DENY"],"description":"Permission level"}}},"McpRemoveRolePolicyParams":{"type":"object","required":["role_id","policy_id"],"properties":{"role_id":{"type":"string","description":"Role ID (UUID)"},"policy_id":{"type":"string","description":"Policy ID (UUID) to remove"}}},"McpGetRoleUsersParams":{"type":"object","required":["role_id"],"properties":{"role_id":{"type":"string","description":"Role ID (UUID)"},"limit":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Results per page"},"after":{"type":"string","description":"Cursor for next page"}}},"McpGetRoleGroupsParams":{"type":"object","required":["role_id"],"properties":{"role_id":{"type":"string","description":"Role ID (UUID)"},"limit":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Results per page"},"after":{"type":"string","description":"Cursor for next page"}}},"McpListFeaturesParams":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":100,"default":50,"description":"Results per page"},"after":{"type":"string","description":"Cursor for next page"},"search":{"type":"string","description":"Filter by name or description"}}},"McpCreateFeatureParams":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Feature name"},"description":{"type":"string","description":"Optional description"}}},"McpUpdateFeatureParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Feature ID (UUID) to update"},"name":{"type":"string","description":"New feature name"},"description":{"type":"string","description":"New description"}}},"McpDeprecateFeatureParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Feature ID (UUID) to deprecate"}}},"HubspotCompany":{"type":"object","required":["id","name","domain","url"],"properties":{"id":{"type":"string","description":"HubSpot company record id"},"name":{"type":["string","null"]},"domain":{"type":["string","null"]},"url":{"type":"string","format":"uri","description":"Deep link to the record in HubSpot"}}},"HubspotCompanyMappingResponse":{"type":"object","required":["clientId","mapping","suggestedQuery"],"properties":{"clientId":{"type":"string","format":"uuid"},"suggestedQuery":{"type":"string","description":"The client's name; pre-fill for the company search (HubSpot matches name, domain, website, and phone)"},"mapping":{"type":["object","null"],"required":["hubspotCompanyId","hubspotCompanyName","hubspotCompanyDomain","portalId","hubspotCompanyUrl","integrationStatus","syncStatus","lastSyncDate","lastSyncValue","lastSyncError","lastSyncWindowStart","lastSyncWindowEnd","mappedDate"],"properties":{"hubspotCompanyId":{"type":"string"},"hubspotCompanyName":{"type":["string","null"]},"hubspotCompanyDomain":{"type":["string","null"]},"portalId":{"type":"string"},"hubspotCompanyUrl":{"type":"string","format":"uri"},"integrationStatus":{"type":"string","enum":["ENABLED","DISABLED"],"description":"DISABLED after HubSpot reported the company gone; re-link to re-enable"},"syncStatus":{"type":"string","enum":["SUCCESS","ERROR","PENDING"]},"lastSyncDate":{"type":["string","null"],"format":"date-time"},"lastSyncValue":{"type":["integer","null"],"description":"Last chat_volume_weekly value pushed"},"lastSyncError":{"type":["string","null"]},"lastSyncWindowStart":{"type":["string","null"],"format":"date-time"},"lastSyncWindowEnd":{"type":["string","null"],"format":"date-time"},"mappedDate":{"type":"string","format":"date-time"}}}}},"McpSearchHubspotCompaniesParams":{"type":"object","required":["q"],"properties":{"q":{"type":"string","minLength":2,"maxLength":100,"description":"Search text"}}},"McpGetHubspotCompanyMappingParams":{"type":"object","required":["clientId"],"properties":{"clientId":{"type":"string","format":"uuid"}}},"McpSetHubspotCompanyMappingParams":{"type":"object","required":["clientId","hubspotCompanyId"],"properties":{"clientId":{"type":"string","format":"uuid"},"hubspotCompanyId":{"type":"string","pattern":"^\\d{1,32}$"},"hubspotCompanyName":{"type":["string","null"],"maxLength":256},"hubspotCompanyDomain":{"type":["string","null"],"maxLength":256}}},"McpRemoveHubspotCompanyMappingParams":{"type":"object","required":["clientId"],"properties":{"clientId":{"type":"string","format":"uuid"}}},"McpPushHubspotChatVolumeParams":{"type":"object","required":["clientId"],"properties":{"clientId":{"type":"string","format":"uuid"},"dryRun":{"type":"boolean","description":"If true, count chats without writing to HubSpot"}}},"McpCreateHubspotCompanyParams":{"type":"object","required":["clientId","name"],"properties":{"clientId":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":256,"description":"Company name to create in HubSpot"},"domain":{"type":["string","null"],"maxLength":256,"description":"Company domain; null or omit if unknown"}}},"HubspotCompanySearchResult":{"type":"object","required":["items","query","fallbackQuery"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/HubspotCompany"},"description":"Matching HubSpot companies (up to 10)"},"query":{"type":"string","description":"The trimmed search term that was queried"},"fallbackQuery":{"type":["string","null"],"description":"When set, the original query returned zero results and this shorter word was used instead"}}},"HubspotCompanyCreateResult":{"type":"object","required":["company","clientId","mapping","suggestedQuery"],"properties":{"company":{"$ref":"#/components/schemas/HubspotCompany"},"clientId":{"type":"string","format":"uuid"},"suggestedQuery":{"type":"string"},"mapping":{"type":"object","required":["hubspotCompanyId","hubspotCompanyName","hubspotCompanyDomain","portalId","hubspotCompanyUrl","integrationStatus","syncStatus","lastSyncDate","lastSyncValue","lastSyncError","lastSyncWindowStart","lastSyncWindowEnd","mappedDate"],"properties":{"hubspotCompanyId":{"type":"string"},"hubspotCompanyName":{"type":["string","null"]},"hubspotCompanyDomain":{"type":["string","null"]},"portalId":{"type":"string"},"hubspotCompanyUrl":{"type":"string","format":"uri"},"integrationStatus":{"type":"string","enum":["ENABLED","DISABLED"]},"syncStatus":{"type":"string","enum":["SUCCESS","ERROR","PENDING"]},"lastSyncDate":{"type":["string","null"],"format":"date-time"},"lastSyncValue":{"type":["integer","null"]},"lastSyncError":{"type":["string","null"]},"lastSyncWindowStart":{"type":["string","null"],"format":"date-time"},"lastSyncWindowEnd":{"type":["string","null"],"format":"date-time"},"mappedDate":{"type":"string","format":"date-time"}}}}},"HubspotSyncRow":{"type":"object","required":["clientId","clientName","hubspotCompanyId","hubspotCompanyName","hubspotCompanyUrl","count","status"],"properties":{"clientId":{"type":"string","format":"uuid"},"clientName":{"type":"string"},"hubspotCompanyId":{"type":"string"},"hubspotCompanyName":{"type":["string","null"]},"hubspotCompanyUrl":{"type":"string","format":"uri"},"count":{"type":["integer","null"],"description":"Chat volume for the week"},"status":{"type":"string","enum":["updated","not_found","failed","skipped","dry_run"]},"error":{"type":"string"}}},"HubspotClientPushResult":{"type":"object","required":["clientId","windowStart","windowEnd","dryRun","row","mapping"],"properties":{"clientId":{"type":"string","format":"uuid"},"windowStart":{"type":"string","format":"date-time"},"windowEnd":{"type":"string","format":"date-time"},"dryRun":{"type":"boolean"},"row":{"$ref":"#/components/schemas/HubspotSyncRow"},"mapping":{"oneOf":[{"$ref":"#/components/schemas/HubspotCompanyMappingResponse"},{"type":"null"}]}}},"McpListUserGroupsParams":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Results per page"},"after":{"type":"string","description":"Cursor for next page"},"search":{"type":"string","description":"Filter by name or description"},"order_by":{"type":"string","enum":["name","member_count","created_date"],"default":"created_date","description":"Sort column"},"sort":{"type":"string","enum":["ASC","DESC"],"default":"DESC","description":"Sort direction"}}},"McpGetUserGroupParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"Group ID (UUID)"}}},"McpCreateUserGroupParams":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Group name"},"description":{"type":"string","description":"Optional description"},"client_id":{"type":"string","format":"uuid","description":"Client ID to scope group"}}},"McpUpdateUserGroupParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"Group ID (UUID) to update"},"name":{"type":"string","description":"New group name"},"description":{"type":"string","description":"New description"}}},"McpDeleteUserGroupParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"Group ID (UUID) to delete"}}},"McpUpdateGroupMembersParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"Group ID (UUID)"},"add":{"type":"array","items":{"type":"string","format":"uuid"},"description":"User IDs to add"},"remove":{"type":"array","items":{"type":"string","format":"uuid"},"description":"User IDs to remove"}}},"McpUpdateGroupRolesParams":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"Group ID (UUID)"},"add":{"type":"array","items":{"type":"object","properties":{"role_id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid"}}},"description":"Role assignments to add"},"remove":{"type":"array","items":{"type":"object","properties":{"role_id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid"}}},"description":"Role assignments to remove"}}},"McpListRegionsParams":{"type":"object","required":["client_id"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"limit":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Page size"},"after":{"type":"string","description":"Cursor for next page"},"search":{"type":"string","description":"Search by name or description"}}},"McpCreateRegionParams":{"type":"object","required":["client_id","name"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"name":{"type":"string","description":"Region name"},"description":{"type":"string","description":"Optional description"}}},"McpUpdateRegionParams":{"type":"object","required":["client_id","region_id"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"region_id":{"type":"string","description":"Region ID (UUID) to update"},"name":{"type":"string","description":"New region name"},"description":{"type":"string","description":"New description"}}},"McpDeleteRegionParams":{"type":"object","required":["client_id","region_id"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"region_id":{"type":"string","description":"Region ID (UUID) to disable"}}},"McpListDepartmentsParams":{"type":"object","required":["client_id"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"limit":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Page size"},"after":{"type":"string","description":"Cursor for next page"},"search":{"type":"string","description":"Search by name or description"}}},"McpCreateDepartmentParams":{"type":"object","required":["client_id","name"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"name":{"type":"string","description":"Department name"},"description":{"type":"string","description":"Optional description"}}},"McpUpdateDepartmentParams":{"type":"object","required":["client_id","department_id"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"department_id":{"type":"string","description":"Department ID (UUID) to update"},"name":{"type":"string","description":"New department name"},"description":{"type":"string","description":"New description"}}},"McpDeleteDepartmentParams":{"type":"object","required":["client_id","department_id"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"department_id":{"type":"string","description":"Department ID (UUID) to disable"}}},"McpListTagsParams":{"type":"object","required":["client_id"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"limit":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Page size"},"after":{"type":"string","description":"Cursor for next page"},"search":{"type":"string","description":"Search by name or description"}}},"McpCreateTagParams":{"type":"object","required":["client_id","name"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"name":{"type":"string","description":"Tag name"},"description":{"type":"string","description":"Optional description"}}},"McpUpdateTagParams":{"type":"object","required":["client_id","tag_id"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"tag_id":{"type":"string","description":"Tag ID (UUID) to update"},"name":{"type":"string","description":"New tag name"},"description":{"type":"string","description":"New description"}}},"McpDeleteTagParams":{"type":"object","required":["client_id","tag_id"],"properties":{"client_id":{"type":"string","description":"Client UUID"},"tag_id":{"type":"string","description":"Tag ID (UUID) to delete"}}},"McpListClientPipelineTasksParams":{"type":"object","required":["clientId"],"properties":{"clientId":{"type":"string","description":"Client UUID to list running pipeline tasks for"}}},"McpStopClientPipelineTaskParams":{"type":"object","required":["clientId","taskId"],"properties":{"clientId":{"type":"string","description":"Client UUID that owns the pipeline task"},"taskId":{"type":"string","pattern":"^[0-9a-f]{32}$","description":"Pipeline task identifier (32 lowercase hex characters)"}}},"McpListKnowledgeGapsParams":{"type":"object","required":["clientId"],"properties":{"clientId":{"type":"string","description":"Client/tenant ID"},"status":{"type":"string","enum":["OPEN","ANSWERED","DISMISSED","CLOSED"],"description":"Filter by gap lifecycle status. CLOSED is a grouping meaning resolved (ANSWERED + DISMISSED), not a stored status."},"limit":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Results per page (1–200)"},"paginationtoken":{"type":"string","description":"Opaque cursor from a previous response's paginationtoken"}}},"McpRunKnowledgeGapDetectionParams":{"type":"object","required":["clientId"],"properties":{"clientId":{"type":"string","description":"Client/tenant ID"}}},"McpDiagnosticStartParams":{"type":"object","required":["url"],"properties":{"url":{"type":"string","description":"Site URL to diagnose (e.g. https://myhotel.com)"}}},"McpDiagnosticGetSessionParams":{"type":"object","required":["sessionId"],"properties":{"sessionId":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"Opaque session identifier returned by the start-diagnostic response"}}},"McpDiagnosticChatParams":{"type":"object","required":["sessionId","message"],"properties":{"sessionId":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"Opaque session identifier returned by the start-diagnostic response"},"message":{"type":"string","description":"Follow-up question or message"}}},"SiteCheckBody":{"type":"object","required":["url"],"properties":{"url":{"type":"string","description":"The website URL to analyze","example":"https://myhotel.com"}},"example":{"url":"https://myhotel.com"}},"ClientIdCheck":{"type":"object","description":"Extraction + classification of the clientId embedded on the analyzed page. Defect statuses name real copy mistakes (stray whitespace, commas, truncation); corrected carries the best-guess corrected value when the defect is inferable and the result would be a valid UUID.","properties":{"raw":{"type":"string","nullable":true},"status":{"type":"string","enum":["valid","whitespace","embedded_comma","truncated","not_a_uuid","missing"]},"corrected":{"type":"string","nullable":true}}},"SiteCheckResponse":{"type":"object","properties":{"url":{"type":"string"},"fetchable":{"type":"boolean"},"framework":{"type":"string"},"hasAgentScript":{"type":"boolean"},"scriptPlacement":{"type":"string","nullable":true,"enum":["head","body"]},"hasInitCall":{"type":"boolean"},"initCorrect":{"type":"boolean"},"clientIdPresent":{"type":"boolean"},"clientId":{"$ref":"#/components/schemas/ClientIdCheck"},"diagnosis":{"type":"string","enum":["not_embedded","init_missing","already_correct","partial"]},"suggestedPlatformGuide":{"type":"string"},"scriptReachable":{"type":"boolean","nullable":true,"description":"Whether the inject-bundle script URL responded successfully. Null when the page has no inject-bundle script tag or its URL could not be resolved."},"cspBlocksWidget":{"type":"boolean","nullable":true,"description":"Whether a Content-Security-Policy detected on the page would block the inject-bundle script host. Null when no CSP is present or the bundle URL is unavailable. True only when the script host is positively absent from the effective script-src or default-src directive."}}},"DiagnosticStartBody":{"type":"object","required":["url"],"properties":{"url":{"type":"string","description":"The website URL to diagnose","example":"https://myhotel.com"}},"example":{"url":"https://myhotel.com"}},"DiagnosticEvent":{"type":"object","required":["sessionId","runId","type","datetime","url","payload"],"properties":{"sessionId":{"type":"string","description":"Opaque session identifier for the checked site; obtain it from the start-diagnostic response"},"runId":{"type":"string","format":"uuid","description":"Unique run identifier"},"type":{"type":"string","enum":["run_started","progress","finding","run_completed","run_failed","chat_user","chat_assistant"]},"datetime":{"type":"integer","description":"Unix ms timestamp"},"url":{"type":"string","description":"Normalized URL being diagnosed"},"payload":{"type":"object","additionalProperties":true}}},"DiagnosticStartResponse":{"type":"object","required":["sessionId","runId","url","status","channel"],"properties":{"sessionId":{"type":"string"},"runId":{"type":"string","format":"uuid"},"url":{"type":"string"},"status":{"type":"string","enum":["started","already_running"]},"channel":{"type":"string","description":"Real-time events channel path; subscribe to this channel to receive progress and findings as they arrive"}}},"DiagnosticSessionResponse":{"type":"object","required":["sessionId","url","events"],"properties":{"sessionId":{"type":"string"},"url":{"type":"string","nullable":true},"events":{"type":"array","items":{"$ref":"#/components/schemas/DiagnosticEvent"}}}},"DiagnosticChatBody":{"type":"object","required":["message"],"properties":{"message":{"type":"string","minLength":1,"description":"Follow-up chat message","example":"What issues were found with the script placement?"}},"example":{"message":"What issues were found with the script placement?"}},"DiagnosticChatResponse":{"type":"object","required":["message"],"properties":{"message":{"type":"string","description":"Assistant reply"}}},"ScheduleEnum":{"type":"string","enum":["NONE","DAILY","WEEKLY","BI_WEEKLY","MONTHLY"],"description":"Scheduling frequency for analytics or AI visibility runs"},"Client":{"type":"object","required":["id","name","website_id","demo_client","created_date"],"properties":{"id":{"type":"string","format":"uuid","description":"Client UUID"},"name":{"type":"string","description":"Client display name"},"website_id":{"type":"string","format":"uuid","description":"Primary website UUID"},"demo_client":{"type":"boolean","description":"Whether this is a demo client"},"description":{"type":"string","nullable":true,"description":"Optional description of the client"},"scrape_language":{"type":"string","nullable":true,"description":"BCP-47 language code for website scraping (e.g. en, fr)"},"chat_logs_analytics_schedule":{"$ref":"#/components/schemas/ScheduleEnum","description":"Frequency of chat log analytics runs"},"ai_visibility_schedule":{"$ref":"#/components/schemas/ScheduleEnum","description":"Frequency of AI visibility runs"},"manual_knowledge":{"type":"boolean","description":"When true the client's KB is curated manually; automated scraping is suppressed. MA-1158."},"multi_property":{"type":"boolean","description":"When true this client's website is a multi-property group/brand site whose knowledge base is partitioned across child clients. MA-1218."},"last_scraped_date":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp of the most recent website scrape (from linked website)"},"created_date":{"type":"string","format":"date-time","description":"Timestamp when the client was created"},"created_by":{"type":"string","nullable":true,"description":"User ID of the creator"},"disabled_date":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp when the client was disabled (null if active)"},"disabled_by":{"type":"string","nullable":true,"description":"User ID who disabled the client"},"_computed":{"type":"object","nullable":true,"description":"Server-computed display fields derived from the client row. Present on single-client responses only.","properties":{"initials":{"type":"string","description":"First two characters of the client name, uppercased (e.g. 'HO'). '?' when name is empty."},"status_label":{"type":"string","enum":["Active","Demo","Disabled"],"description":"Human-readable status. Disabled > Demo > Active precedence."},"status_variant":{"type":"string","enum":["success","warning","danger"],"description":"Badge variant matching status_label."},"website_url":{"type":"string","nullable":true,"description":"Resolved website URL. Null when not available."},"website_hidden":{"type":"boolean","description":"True when website_url is null (hides the website link in the UI)."},"demo_hidden":{"type":"boolean","description":"True when demo_client is false (hides the Demo badge in the UI)."},"is_disabled":{"type":"boolean","description":"True when disabled_date is non-null."},"primary_action_label":{"type":"string","description":"Label for the primary CTA button: 'Enable' when disabled, 'Edit' otherwise."},"disable_label":{"type":"string","description":"Label for the disable toggle action: 'Enable Client' when disabled, 'Disable Client' otherwise."},"created_date_formatted":{"type":"string","description":"created_date formatted for display (e.g. 'Jan 1, 2024')."}},"required":["initials","status_label","status_variant","website_url","website_hidden","demo_hidden","is_disabled","primary_action_label","disable_label","created_date_formatted"]}}},"ClientsSummaryResponse":{"type":"object","required":["totalActive","last90Days"],"properties":{"totalActive":{"type":"integer","description":"Number of active (non-disabled) clients"},"last90Days":{"type":"integer","description":"Number of clients created within the last 90 days (still active)"}}},"ClientMetricsResponse":{"type":"object","required":["clientId","metrics"],"properties":{"clientId":{"type":"string","format":"uuid"},"metrics":{"type":"object","description":"Placeholder for Overview-tab metrics. Empty today; populated in a later release."}}},"RescrapeClientBody":{"type":"object","properties":{"language":{"type":"string","minLength":1,"maxLength":50,"nullable":true,"description":"Optional override for the scrape language (e.g. 'English', 'French'). Defaults to the client's configured scrape_language, then 'English'."},"force":{"type":"boolean","description":"When true, bypasses the manual_knowledge guard and triggers a scrape even for manually-managed clients. Defaults to false. MA-1158."}}},"RescrapeClientResponse":{"type":"object","required":["scrapeSessionId","enqueued"],"properties":{"scrapeSessionId":{"type":"string","format":"uuid"},"enqueued":{"type":"boolean","description":"true when the scrape task was accepted for processing; false in environments where task dispatch is not configured — the session is recorded but will not run automatically."}}},"DemoSiteBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"Override name used to slugify the preview-site folder."},"website":{"type":"string","format":"uri","description":"Override client website rendered inside the preview iframe. Defaults to the client's configured website."}}},"DemoSiteResponse":{"type":"object","required":["previewUrl","folderName"],"properties":{"previewUrl":{"type":"string","format":"uri"},"folderName":{"type":"string"}}},"ClientListResponse":{"type":"object","required":["items","total","limit","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Client"},"description":"Page of client records"},"total":{"type":"integer","description":"Total number of matching clients (across all pages)"},"limit":{"type":"integer","description":"Page size used for this request"},"nextCursor":{"type":"string","nullable":true,"description":"Opaque cursor for the next page. Null when there are no more pages."},"hasMore":{"type":"boolean","description":"Whether additional pages exist after this one"}}},"CreateClientBody":{"type":"object","required":["name","website_id"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"Client name"},"website_id":{"type":"string","format":"uuid","description":"UUID of the primary website"},"demo_client":{"type":"boolean","default":false,"description":"Whether this client is a demo"},"description":{"type":"string","maxLength":1000,"nullable":true,"description":"Optional client description"},"scrape_language":{"type":"string","maxLength":10,"nullable":true,"description":"BCP-47 language code for scraping (e.g. en, fr)"},"chat_logs_analytics_schedule":{"$ref":"#/components/schemas/ScheduleEnum","default":"NONE"},"ai_visibility_schedule":{"$ref":"#/components/schemas/ScheduleEnum","default":"NONE"},"manual_knowledge":{"type":"boolean","default":false,"description":"When true the client's KB is curated manually. The automated website-sync fan-out skips this client, and on-demand scrape triggers refuse unless force=true is passed. MA-1158."},"multi_property":{"type":"boolean","default":false,"description":"When true this client's website is a multi-property group/brand site: property DISCOVERY runs instead of a whole-site scrape, and the knowledge base is partitioned across the child clients discovery proposes. Setting it also FORCES manual_knowledge — a group site must never be scraped whole into the parent's own namespace, the decision has to be made in the same call that creates the client (a client only becomes a parent afterwards, via /:id/children), and there is no undo that clears an already-ingested KB. MA-1218."}}},"UserStatus":{"type":"string","enum":["active","disabled","invited","expired"],"description":"Computed user status derived from disabled_date, accepted_date, and invite_expiry_date"},"UserListItem":{"type":"object","required":["id","email","first_name","last_name","status","created_date"],"properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"first_name":{"type":"string"},"last_name":{"type":"string"},"phone":{"type":"string","nullable":true},"status":{"$ref":"#/components/schemas/UserStatus"},"roles":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"client_name":{"type":"string","nullable":true}}}},"groups":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}}},"api_key_count":{"type":"integer"},"last_login_date":{"type":"string","format":"date-time","nullable":true},"disabled_date":{"type":"string","format":"date-time","nullable":true},"created_date":{"type":"string","format":"date-time"},"invited_by_email":{"type":"string","nullable":true},"invite_expiry_date":{"type":"string","format":"date-time","nullable":true},"accepted_date":{"type":"string","format":"date-time","nullable":true}}},"UserListResponse":{"type":"object","required":["items","total","limit","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/UserListItem"}},"total":{"type":"integer"},"limit":{"type":"integer"},"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}}},"InviteUserBody":{"type":"object","required":["email","first_name","last_name"],"properties":{"email":{"type":"string","format":"email","maxLength":254},"first_name":{"type":"string","minLength":1,"maxLength":100},"last_name":{"type":"string","minLength":1,"maxLength":100},"role_ids":{"type":"array","items":{"type":"object","properties":{"role_id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid","nullable":true}}}},"group_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"invite_expiry_days":{"type":"integer","minimum":1,"maximum":90,"default":7}}},"UpdateUserBody":{"type":"object","properties":{"first_name":{"type":"string","minLength":1,"maxLength":100},"last_name":{"type":"string","minLength":1,"maxLength":100},"phone":{"type":"string","maxLength":30,"nullable":true},"enable":{"type":"boolean"}}},"InviteUserResponse":{"type":"object","required":["id","email","invite_expiry_date"],"properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"invite_expiry_date":{"type":"string","format":"date-time"}}},"ReinviteUserBody":{"type":"object","properties":{"invite_expiry_days":{"type":"integer","minimum":1,"maximum":90,"default":7}}},"ForceResetResponse":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}},"ActivityEvent":{"type":"object","required":["event_date","action"],"properties":{"event_date":{"type":"string","format":"date-time","description":"ISO 8601 timestamp of when the event occurred"},"action":{"type":"string","enum":["login","role_assigned","role_removed","group_added","group_removed","api_key_created","api_key_disabled","invited","invite_accepted","disabled"],"description":"Type of activity event"},"actor_email":{"type":"string","nullable":true,"description":"Email of the user who performed the action (null if performed by the user themselves or unknown)"},"detail":{"type":"string","nullable":true,"description":"Additional context (e.g. role name, group name, API key description)"}}},"UserActivityResponse":{"type":"object","required":["items","hasMore","nextCursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ActivityEvent"},"description":"Page of activity events ordered by event_date DESC"},"hasMore":{"type":"boolean","description":"Whether additional pages exist after this one"},"nextCursor":{"type":"string","nullable":true,"description":"Opaque cursor for the next page. Null when there are no more pages."}}},"RoleListItem":{"type":"object","required":["id","name","description","visibility_maison_only","policy_count","user_count","group_count","created_date"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"client_id":{"type":"string","nullable":true},"client_name":{"type":"string","nullable":true},"visibility_maison_only":{"type":"boolean","nullable":true},"policy_count":{"type":"integer"},"user_count":{"type":"integer"},"group_count":{"type":"integer"},"created_date":{"type":"string","format":"date-time"},"disabled_date":{"type":"string","format":"date-time","nullable":true}}},"RoleDetail":{"type":"object","required":["id","name","description","visibility_maison_only","policy_count","user_count","group_count","created_date","policies"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"client_id":{"type":"string","nullable":true},"client_name":{"type":"string","nullable":true},"visibility_maison_only":{"type":"boolean","nullable":true},"policy_count":{"type":"integer"},"user_count":{"type":"integer"},"group_count":{"type":"integer"},"created_date":{"type":"string","format":"date-time"},"disabled_date":{"type":"string","format":"date-time","nullable":true},"created_by_email":{"type":"string","nullable":true},"disabled_by_email":{"type":"string","nullable":true},"policies":{"type":"array","items":{"type":"object","required":["id","permission","created_date"],"properties":{"id":{"type":"string","format":"uuid"},"feature_id":{"type":"string","nullable":true},"feature_name":{"type":"string","nullable":true},"client_id":{"type":"string","nullable":true},"client_name":{"type":"string","nullable":true},"client_region_id":{"type":"string","nullable":true},"region_name":{"type":"string","nullable":true},"client_department_id":{"type":"string","nullable":true},"department_name":{"type":"string","nullable":true},"tag_id":{"type":"string","nullable":true},"tag_name":{"type":"string","nullable":true},"permission":{"type":"string","enum":["READ","WRITE","EXECUTE","DENY"]},"created_date":{"type":"string","format":"date-time"},"created_by_email":{"type":"string","nullable":true}}}}}},"RoleListResponse":{"type":"object","required":["items","total","limit","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RoleListItem"}},"total":{"type":"integer"},"limit":{"type":"integer"},"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}}},"CreateRoleBody":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"client_id":{"type":"string","nullable":true},"visibility_maison_only":{"type":"boolean"}}},"UpdateRoleBody":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"visibility_maison_only":{"type":"boolean"}}},"AddPolicyBody":{"type":"object","required":["permission"],"properties":{"feature_id":{"type":"string","nullable":true},"client_id":{"type":"string","nullable":true},"client_region_id":{"type":"string","nullable":true},"client_department_id":{"type":"string","nullable":true},"tag_id":{"type":"string","nullable":true},"permission":{"type":"string","enum":["READ","WRITE","EXECUTE","DENY"]}}},"RoleUsersResponse":{"type":"object","required":["items","total","limit","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"type":"object","required":["id","email","assigned_date"],"properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"first_name":{"type":"string"},"last_name":{"type":"string"},"client_id":{"type":"string","nullable":true},"client_name":{"type":"string","nullable":true},"assigned_date":{"type":"string","format":"date-time"},"assigned_by_email":{"type":"string","nullable":true}}}},"total":{"type":"integer"},"limit":{"type":"integer"},"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}}},"RoleGroupsResponse":{"type":"object","required":["items","total","limit","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"type":"object","required":["id","name","assigned_date"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"client_id":{"type":"string","nullable":true},"client_name":{"type":"string","nullable":true},"assigned_date":{"type":"string","format":"date-time"},"assigned_by_email":{"type":"string","nullable":true}}}},"total":{"type":"integer"},"limit":{"type":"integer"},"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}}},"UserGroupMemberItem":{"type":"object","required":["user_id","email","first_name","last_name","status","joined_date"],"properties":{"user_id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"first_name":{"type":"string"},"last_name":{"type":"string"},"status":{"type":"string","enum":["active","disabled","invited","expired"]},"joined_date":{"type":"string","format":"date-time"},"added_by_email":{"type":"string","nullable":true}}},"UserGroupRoleItem":{"type":"object","required":["role_id","role_name","assigned_date"],"properties":{"role_id":{"type":"string","format":"uuid"},"role_name":{"type":"string"},"client_id":{"type":"string","format":"uuid","nullable":true},"client_name":{"type":"string","nullable":true},"assigned_date":{"type":"string","format":"date-time"},"assigned_by_email":{"type":"string","nullable":true}}},"UserGroupListItem":{"type":"object","required":["id","name","member_count","role_count","created_date"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"client_id":{"type":"string","format":"uuid","nullable":true},"client_name":{"type":"string","nullable":true},"member_count":{"type":"integer"},"role_count":{"type":"integer"},"created_date":{"type":"string","format":"date-time"}}},"UserGroupDetail":{"type":"object","required":["id","name","created_date","members","roles","member_count","role_count"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"client_id":{"type":"string","format":"uuid","nullable":true},"client_name":{"type":"string","nullable":true},"created_date":{"type":"string","format":"date-time"},"created_by_email":{"type":"string","nullable":true},"deleted_date":{"type":"string","format":"date-time","nullable":true},"members":{"type":"array","items":{"$ref":"#/components/schemas/UserGroupMemberItem"}},"roles":{"type":"array","items":{"$ref":"#/components/schemas/UserGroupRoleItem"}},"member_count":{"type":"integer"},"role_count":{"type":"integer"}}},"UserGroupListResponse":{"type":"object","required":["items","total","limit","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/UserGroupListItem"}},"total":{"type":"integer"},"limit":{"type":"integer"},"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}}},"CreateUserGroupBody":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"client_id":{"type":"string","format":"uuid"}}},"UpdateUserGroupBody":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"UpdateGroupMembersBody":{"type":"object","properties":{"add":{"type":"array","items":{"type":"string","format":"uuid"}},"remove":{"type":"array","items":{"type":"string","format":"uuid"}}}},"UpdateGroupRolesBody":{"type":"object","properties":{"add":{"type":"array","items":{"type":"object","required":["role_id"],"properties":{"role_id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid"}}}},"remove":{"type":"array","items":{"type":"object","required":["role_id"],"properties":{"role_id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid"}}}}}},"UpdateGroupMembersResponse":{"type":"object","required":["added","removed"],"properties":{"added":{"type":"integer"},"removed":{"type":"integer"}}},"UpdateGroupRolesResponse":{"type":"object","required":["added","removed"],"properties":{"added":{"type":"integer"},"removed":{"type":"integer"}}},"FeatureItem":{"type":"object","required":["id","name","description","deprecated"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"deprecated":{"type":"boolean"}}},"FeatureListResponse":{"type":"object","required":["items","total","limit","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/FeatureItem"}},"total":{"type":"integer"},"limit":{"type":"integer"},"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}}},"CreateFeatureBody":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":"string"}}},"UpdateFeatureBody":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"RegionListItem":{"type":"object","required":["id","name","description","created_date"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"created_date":{"type":"string","format":"date-time"}}},"RegionListResponse":{"type":"object","required":["items","total","limit","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RegionListItem"}},"total":{"type":"integer"},"limit":{"type":"integer"},"nextCursor":{"type":["string","null"]},"hasMore":{"type":"boolean"}}},"CreateRegionBody":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":1000}}},"UpdateRegionBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":1000}}},"DepartmentListItem":{"type":"object","required":["id","name","description","created_date"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"created_date":{"type":"string","format":"date-time"}}},"DepartmentListResponse":{"type":"object","required":["items","total","limit","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/DepartmentListItem"}},"total":{"type":"integer"},"limit":{"type":"integer"},"nextCursor":{"type":["string","null"]},"hasMore":{"type":"boolean"}}},"CreateDepartmentBody":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":1000}}},"UpdateDepartmentBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":1000}}},"TagListItem":{"type":"object","required":["id","name","description","created_date"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"created_date":{"type":"string","format":"date-time"}}},"TagListResponse":{"type":"object","required":["items","total","limit","nextCursor","hasMore"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TagListItem"}},"total":{"type":"integer"},"limit":{"type":"integer"},"nextCursor":{"type":["string","null"]},"hasMore":{"type":"boolean"}}},"CreateTagBody":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":1000}}},"UpdateTagBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":1000}}},"KnowledgeGap":{"type":"object","required":["id","client_id","source","canonical_question","status","occurrence_count","first_seen","last_seen"],"properties":{"id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid"},"session_id":{"type":["string","null"],"format":"uuid"},"source":{"type":"string","enum":["GUEST_CHATLOG","GFAQ_SCAN"]},"canonical_question":{"type":"string"},"language":{"type":["string","null"]},"status":{"type":"string","enum":["OPEN","ANSWERED","DISMISSED"]},"occurrence_count":{"type":"integer"},"first_seen":{"type":"string","format":"date-time"},"last_seen":{"type":"string","format":"date-time"},"sample_refs":{"type":["array","null"],"items":{"type":"object","additionalProperties":true},"description":"Session/conversation references + question variants backing the gap"},"answered_qa_id":{"type":["string","null"]},"resolved_by":{"type":["string","null"]},"resolved_date":{"type":["string","null"],"format":"date-time"},"created_date":{"type":"string","format":"date-time"},"created_by":{"type":["string","null"]}}},"KnowledgeGapLastSession":{"type":"object","required":["id","client_id","source","window_start","window_end","trigger","status"],"properties":{"id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid"},"source":{"type":"string","enum":["GUEST_CHATLOG","GFAQ_SCAN"]},"window_start":{"type":"string","format":"date-time"},"window_end":{"type":"string","format":"date-time"},"trigger":{"type":"string","enum":["SCHEDULED","MANUAL"]},"status":{"type":"string","enum":["RUNNING","SUCCESS","ERROR"]},"error":{"type":["string","null"]},"completed_date":{"type":["string","null"],"format":"date-time"},"created_date":{"type":"string","format":"date-time"},"created_by":{"type":["string","null"]}}},"KnowledgeGapListResponse":{"type":"object","required":["gaps","lastSession"],"properties":{"gaps":{"type":"array","items":{"$ref":"#/components/schemas/KnowledgeGap"}},"lastSession":{"oneOf":[{"$ref":"#/components/schemas/KnowledgeGapLastSession"},{"type":"null"}]},"paginationtoken":{"type":"string","description":"Opaque keyset cursor — present only when more rows exist"}}},"KnowledgeGapAnswerBody":{"type":"object","required":["answer"],"properties":{"answer":{"type":"string","minLength":1,"maxLength":10000}}},"PropertyDiscoveryCandidate":{"type":"object","description":"One property the discovery run proposes creating as a child client. Machine-derived fields (pageCount, confidence, evidence) explain WHY it was proposed; the editable fields (name, city, country, address, urlPrefixes) are what provisioning actually uses.","required":["id","candidateKey","name","propertyUrl","urlPrefixes","pageCount","samplePages","confidence","decision","provisionStatus"],"properties":{"id":{"type":"string","format":"uuid"},"candidateKey":{"type":"string","description":"Stable identity within the run — the normalised path prefix or host"},"name":{"type":"string"},"propertyUrl":{"type":"string","format":"uri"},"urlPrefixes":{"type":"array","items":{"type":"string","format":"uri"},"description":"URL prefixes belonging to this property — plural because a brand site repeats each property per locale (/en/..., /fr/...)"},"pageCount":{"type":"integer"},"samplePages":{"type":"array","items":{"type":"string","format":"uri"}},"city":{"type":["string","null"]},"country":{"type":["string","null"]},"address":{"type":["string","null"]},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"How sure discovery is that this is a real, distinct property"},"evidence":{"oneOf":[{"type":"object"},{"type":"null"}],"description":"Heuristic signals plus the verifier's stated reason"},"decision":{"type":"string","enum":["PENDING","APPROVED","REJECTED"]},"provisionStatus":{"type":"string","enum":["PENDING","PROVISIONED","FAILED"]},"provisionError":{"type":["string","null"]},"childClientId":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"The client created for this property, once provisioning has succeeded"}}},"PropertyDiscoverySession":{"type":"object","description":"One discovery run against a multi-property group website. Status flows DISCOVERING → AWAITING_REVIEW → PROVISIONING → COMPLETED/FAILED; the run is also the progress record the console polls.","required":["id","clientId","websiteId","status","seedUrl","createdDate","candidates"],"properties":{"id":{"type":"string","format":"uuid"},"clientId":{"type":"string","format":"uuid"},"websiteId":{"type":"string","format":"uuid"},"scrapeSessionId":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"The website scrape session carrying the pipeline's live step progress for this run"},"status":{"type":"string","enum":["DISCOVERING","AWAITING_REVIEW","PROVISIONING","COMPLETED","FAILED"]},"seedUrl":{"type":"string","format":"uri"},"summary":{"oneOf":[{"type":"object"},{"type":"null"}],"description":"Run summary from the pipeline, plus the provisioning tallies once approved"},"createdDate":{"type":"string","format":"date-time"},"updatedDate":{"type":["string","null"],"format":"date-time"},"candidates":{"type":"array","items":{"$ref":"#/components/schemas/PropertyDiscoveryCandidate"}}}},"Error":{"type":"object","properties":{"error":{"type":"string"}}},"AgentCard":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"},"capabilities":{"type":"object","properties":{"streaming":{"type":"boolean"},"pushNotifications":{"type":"boolean"}}},"authentication":{"type":"object","properties":{"schemes":{"type":"array","items":{"type":"string"}},"credentials":{"type":"string"}}},"defaultInputModes":{"type":"array","items":{"type":"string"}},"defaultOutputModes":{"type":"array","items":{"type":"string"}},"skills":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}}}}}},"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"HMAC-signed API key"},"sessionAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Maison session token (cookie or Authorization: Bearer header)"}}}}