openapi: 3.0.3
info:
  title: Watchatic Backend V2 Public API
  version: 0.1.0
  description: Public JSON contract for the Watchatic backend v2 foundation.
servers:
  - url: https://api-v2.watchcollect.com
tags:
  - name: health
  - name: ingest
  - name: sessions
  - name: workspaces
  - name: groups
  - name: views
  - name: messages
  - name: outbound
  - name: market
  - name: alerts
  - name: dashboard
  - name: search
  - name: agent
  - name: logs
  - name: actions
  - name: ai
  - name: classifications
paths:
  /v2/healthz:
    get:
      operationId: getHealth
      tags:
        - health
      summary: Get service health
      responses:
        "200":
          description: Service is healthy.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "503":
          description: Service dependency is unhealthy.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
  /v2/webhooks/waha:
    post:
      operationId: receiveWahaWebhook
      tags:
        - ingest
      summary: Capture a raw WAHA webhook event
      security:
        - WahaSecret: []
      parameters:
        - name: userId
          in: query
          required: true
          schema:
            type: string
          example: user_35eh25qkkIVwab803vgofO1uSsl
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WahaWebhookEnvelope"
      responses:
        "202":
          description: Webhook accepted or duplicate accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid webhook payload.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid webhook secret.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Conflicting duplicate webhook body.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Internal ingest failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/webhooks/clerk:
    post:
      operationId: receiveClerkWebhook
      tags:
        - ingest
      summary: Sync Clerk organization and membership events
      security:
        - SvixSignature: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - type
                - data
              properties:
                type:
                  type: string
                  example: organization.created
                data:
                  type: object
                  description: Clerk event payload. Shape depends on the event type; backend reads only organization, membership and invitation identifiers needed for workspace sync.
      responses:
        "202":
          description: Webhook accepted or ignored.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid webhook payload.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid Svix signature.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/sessions:
    get:
      operationId: listSessions
      tags:
        - sessions
      summary: List WAHA sessions linked to the authenticated user
      security:
        - BearerAuth: []
      parameters:
        - name: role
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/SessionRole"
      responses:
        "200":
          description: List of linked WAHA sessions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionPage"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: createSession
      tags:
        - sessions
      summary: Create or link a WAHA session and start auth
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSessionRequest"
      responses:
        "202":
          description: Session created or linked and auth started.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateSessionResponse"
        "400":
          description: Invalid command.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: WAHA session does not belong to the authenticated user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: WAHA session was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: WAHA session is already linked to another tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: WAHA rejected the lifecycle request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateSessionResponse"
  /v2/sessions/{session_id}:
    patch:
      operationId: updateSession
      tags:
        - sessions
      summary: Update a linked WAHA session
      description: Renames the local label for a linked WAHA session. Workspace owners and admins may rename any workspace session; members may rename only sessions they connected.
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchSessionRequest"
      responses:
        "202":
          description: Session update event appended.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Actor cannot rename this workspace session.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/sessions/{session_id}/auth/qr:
    get:
      operationId: refreshSessionQr
      tags:
        - sessions
      summary: Fetch a fresh QR pairing value for a session
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: QR pairing value.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionQrResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session or QR value was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: WAHA rejected the QR request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/sessions/{session_id}/auth/request-code:
    post:
      operationId: requestSessionAuthCode
      tags:
        - sessions
      summary: Request an OTP pairing code for a session
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RequestSessionAuthCodeRequest"
      responses:
        "202":
          description: OTP request accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionAuthCodeResponse"
        "400":
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: WAHA rejected the OTP request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/sessions/{session_id}/start:
    post:
      operationId: startSession
      tags:
        - sessions
      summary: Start an existing WAHA session
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "202":
          description: Session start accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateSessionResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: WAHA rejected the start request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/sessions/{session_id}/uninstall:
    post:
      operationId: uninstallSession
      tags:
        - sessions
      summary: Remove backend-v2 from an existing WAHA session
      description: Removes only the backend-v2 WAHA webhook and soft-deletes the backend-v2 session link. It does not stop or delete the WAHA session.
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "202":
          description: Backend-v2 uninstall accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: WAHA rejected the webhook removal request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/sessions/{session_id}/sync:
    post:
      operationId: syncSession
      tags:
        - sessions
      summary: Request manual receiving session sync
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "202":
          description: Sync request events appended.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/sessions/{session_id}/ai/model-profiles:
    get:
      operationId: listSessionAiModelProfiles
      tags:
        - ai
      summary: List AI model profiles and connector status for a receiving session
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Available model profiles and session-scoped connector status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelProfilesResponse"
        "400":
          description: Session is not a receiving session.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/sessions/{session_id}/ai/connectors/{connector_id}/key:
    put:
      operationId: storeSessionAiConnectorKey
      tags:
        - ai
      summary: Store an AI connector key for a receiving session
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: connector_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/AIConnectorId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConnectorKeyRequest"
      responses:
        "202":
          description: Session connector key stored.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid connector id, session role or encryption configuration.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Caller is not the connecting user for the session.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      operationId: removeSessionAiConnectorKey
      tags:
        - ai
      summary: Remove an AI connector key for a receiving session
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: connector_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/AIConnectorId"
      responses:
        "202":
          description: Session connector key removed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid connector id or session role.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Caller is not the connecting user for the session.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/sessions/{session_id}/classification-settings:
    get:
      operationId: getSessionClassificationSettings
      tags:
        - classifications
      summary: Get classification settings for a receiving session
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Current session-scoped classification settings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClassificationSettings"
        "400":
          description: Session is not a receiving session.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    put:
      operationId: updateSessionClassificationSettings
      tags:
        - classifications
      summary: Update classification settings for a receiving session
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateClassificationSettingsRequest"
      responses:
        "202":
          description: Session settings update accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid model profile id or session role.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Caller is not the connecting user for the session.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/workspaces/{org_id}/session-access:
    get:
      operationId: listWorkspaceSessionAccess
      tags:
        - workspaces
      summary: List session grants for a workspace member
      security:
        - BearerAuth: []
      parameters:
        - name: org_id
          in: path
          required: true
          schema:
            type: string
        - name: member
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Session grants for the member.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceSessionAccessPage"
        "400":
          description: Missing or invalid member query.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Workspace mismatch or missing admin permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: grantWorkspaceSessionAccess
      tags:
        - workspaces
      summary: Grant a workspace member access to a session
      security:
        - BearerAuth: []
      parameters:
        - name: org_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GrantWorkspaceSessionAccessRequest"
      responses:
        "202":
          description: Access grant event appended.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Workspace mismatch or missing admin permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/workspaces/{org_id}/session-access/{session_id}:
    delete:
      operationId: revokeWorkspaceSessionAccess
      tags:
        - workspaces
      summary: Revoke a workspace member's access to a session
      security:
        - BearerAuth: []
      parameters:
        - name: org_id
          in: path
          required: true
          schema:
            type: string
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: member
          in: query
          required: true
          schema:
            type: string
      responses:
        "202":
          description: Access revocation event appended.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Workspace mismatch or missing admin permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/workspaces/{org_id}/sessions/import-personal:
    post:
      operationId: importPersonalWorkspaceSessions
      tags:
        - workspaces
      summary: Import the actor's personal WhatsApp sessions into the active workspace
      security:
        - BearerAuth: []
      parameters:
        - name: org_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ImportPersonalSessionsRequest"
      responses:
        "202":
          description: Import attempted for every matching personal session.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImportPersonalSessionsResponse"
        "400":
          description: Invalid session id list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Workspace mismatch or missing active workspace.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: No personal sessions were available to import.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/workspaces/{org_id}/transfer:
    post:
      operationId: transferWorkspaceOwnership
      tags:
        - workspaces
      summary: Transfer workspace ownership
      security:
        - BearerAuth: []
      parameters:
        - name: org_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TransferWorkspaceOwnershipRequest"
      responses:
        "202":
          description: Ownership transfer event appended.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Workspace mismatch or missing owner permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/workspaces/{org_id}/settings:
    get:
      operationId: getWorkspaceSettings
      tags:
        - workspaces
      summary: Get workspace settings
      security:
        - BearerAuth: []
      parameters:
        - name: org_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Workspace settings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceSettings"
        "403":
          description: Workspace mismatch or missing admin permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Workspace settings not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    patch:
      operationId: patchWorkspaceSettings
      tags:
        - workspaces
      summary: Update workspace settings
      security:
        - BearerAuth: []
      parameters:
        - name: org_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchWorkspaceSettingsRequest"
      responses:
        "202":
          description: Settings update event appended.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Workspace mismatch or missing admin permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/groups:
    get:
      operationId: listGroups
      tags:
        - groups
      summary: List projected WhatsApp groups
      security:
        - BearerAuth: []
      parameters:
        - name: session_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: query
          in: query
          required: false
          schema:
            type: string
        - name: enabled
          in: query
          required: false
          schema:
            type: boolean
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 50
      responses:
        "200":
          description: Cursor page of WhatsApp groups.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GroupPage"
        "400":
          description: Invalid query.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/groups/{group_id}:
    get:
      operationId: getGroup
      tags:
        - groups
      summary: Get projected WhatsApp group detail
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/GroupIdPath"
      responses:
        "200":
          description: WhatsApp group detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GroupDetail"
        "400":
          description: Invalid group id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Group was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/groups/{group_id}/selection:
    put:
      operationId: updateGroupSelection
      tags:
        - groups
      summary: Enable or disable future ingest for a WhatsApp group
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/GroupIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateGroupSelectionRequest"
      responses:
        "202":
          description: Selection command accepted or no-op.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid command.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Group was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/groups/selection:
    put:
      operationId: bulkUpdateGroupSelection
      tags:
        - groups
      summary: Enable or disable future ingest for multiple WhatsApp groups
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BulkUpdateGroupSelectionRequest"
      responses:
        "202":
          description: Bulk selection command accepted or no-op.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid command.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: One or more groups were not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/views:
    get:
      operationId: listViews
      tags:
        - views
      summary: List saved filter views
      description: Returns universal saved filter definitions for the authenticated tenant.
      security:
        - BearerAuth: []
      parameters:
        - name: tab_key
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Saved views for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ViewPage"
        "400":
          description: Invalid query.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: createView
      tags:
        - views
      summary: Create a saved filter view
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateViewRequest"
      responses:
        "202":
          description: Saved view creation event accepted. The read model is updated asynchronously by the saved view projector.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid command.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/views/{view_id}:
    get:
      operationId: getView
      tags:
        - views
      summary: Get a saved filter view
      description: Returns a single saved view by id for the authenticated tenant. Falls back to a system view when the id matches a built-in view.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ViewIdPath"
      responses:
        "200":
          description: The saved view.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ViewItem"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: View was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    patch:
      operationId: patchView
      tags:
        - views
      summary: Update a saved filter view
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ViewIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchViewRequest"
      responses:
        "202":
          description: Saved view update event accepted. The read model is updated asynchronously by the saved view projector.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid command.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: View was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      operationId: deleteView
      tags:
        - views
      summary: Delete a saved filter view
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ViewIdPath"
      responses:
        "202":
          description: Saved view delete event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: View was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/views/{view_id}/preview:
    post:
      operationId: previewView
      tags:
        - views
      summary: Preview a saved filter view
      description: Runs a saved view and returns a cursor page. Currently implemented for message previews.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ViewIdPath"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PreviewViewRequest"
      responses:
        "200":
          description: Cursor page for the saved view.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessagePage"
        "400":
          description: Invalid request or unsupported preview.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: View was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/views/{view_id}/blocked-phones:
    get:
      operationId: getViewBlockedPhones
      tags:
        - views
      summary: List phone numbers excluded by a saved view
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ViewIdPath"
      responses:
        "200":
          description: Excluded phone numbers.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BlockedPhonesResponse"
        "404":
          description: View was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: addViewBlockedPhone
      tags:
        - views
      summary: Add a phone number to a saved view exclusion list
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ViewIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BlockPhoneRequest"
      responses:
        "202":
          description: Phone number exclusion update event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid command.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: View was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/views/{view_id}/blocked-phones/{phone_number}:
    delete:
      operationId: removeViewBlockedPhone
      tags:
        - views
      summary: Remove a phone number from a saved view exclusion list
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ViewIdPath"
        - name: phone_number
          in: path
          required: true
          schema:
            type: string
      responses:
        "202":
          description: Phone number exclusion update event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "404":
          description: View was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/messages:
    get:
      operationId: listMessages
      tags:
        - messages
      summary: List projected canonical messages
      description: Cursor-paginated message feed backed only by read-model tables. Defaults to latest activity first using last_seen_at.
      security:
        - BearerAuth: []
      parameters:
        - name: view_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Saved view filters are combined with request query params using AND semantics.
        - name: q
          in: query
          required: false
          schema:
            type: string
            minLength: 3
          description: Indexed search over message text. Combined with saved-view text_search using AND.
        - name: search_mode
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/MessageSearchMode"
            default: broad
          description: Search behavior for q.
        - name: classification_status
          in: query
          required: false
          schema:
            type: array
            items:
              $ref: "#/components/schemas/ClassificationStatus"
          style: form
          explode: false
          description: Comma-separated classification statuses.
        - name: intent
          in: query
          required: false
          schema:
            type: array
            items:
              $ref: "#/components/schemas/MessageIntent"
          style: form
          explode: false
          description: Comma-separated message intents.
        - name: group_id
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
              format: uuid
          style: form
          explode: false
          description: Comma-separated group IDs.
        - name: contact_id
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
              format: uuid
          style: form
          explode: false
          description: Comma-separated contact IDs.
        - name: country_prefix
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
          style: form
          explode: false
          description: Comma-separated E.164 country phone prefixes without a leading plus sign.
          example: "31,44"
        - name: from_ms
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: to_ms
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: last_seen_from_ms
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: last_seen_to_ms
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: include_revoked
          in: query
          required: false
          schema:
            type: boolean
            default: false
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: Cursor page of canonical messages.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessagePage"
        "400":
          description: Invalid query.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/outbound/sends:
    get:
      operationId: listOutboundSends
      tags:
        - outbound
      summary: List outbound WhatsApp send history
      description: Reads projected outbound send audit rows including stored body/media context and per-recipient delivery state.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OutboundSessionIdQuery"
        - $ref: "#/components/parameters/OutboundStatusQuery"
        - $ref: "#/components/parameters/CursorQuery"
        - $ref: "#/components/parameters/PageSizeQuery"
      responses:
        "200":
          description: Cursor page of outbound sends.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutboundTextSendPage"
        "400":
          description: Invalid query.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: createOutboundSend
      tags:
        - outbound
      summary: Create an immediate or scheduled outbound WhatsApp send
      description: Accepts a v2 outbound send command with text, image media, uploaded image files, or both. Without a schedule it queues one send job per recipient immediately. With a schedule it appends a scheduled-send event and starts a Temporal workflow that dispatches one or more occurrences later.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOutboundSendRequest"
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/CreateOutboundSendMultipartRequest"
            encoding:
              group_ids:
                style: form
                explode: true
              files:
                style: form
                explode: true
      responses:
        "202":
          description: Send command accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateOutboundSendResponse"
        "400":
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Sending session was not found for the authenticated workspace.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Outbound media storage is not configured for multipart file uploads.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Sending session is not ready or idempotency key conflicts with a different request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/outbound/sends/{send_id}:
    get:
      operationId: getOutboundSend
      tags:
        - outbound
      summary: Get outbound WhatsApp send detail
      description: Reads one projected outbound send with per-recipient delivery state.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OutboundSendIdPath"
      responses:
        "200":
          description: Outbound send detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutboundTextSend"
        "400":
          description: Invalid send id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Outbound send was not found for the authenticated workspace.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      operationId: deleteOutboundSend
      tags:
        - outbound
      summary: Delete one outbound WhatsApp send from history
      description: Soft-deletes one outbound send audit row for the authenticated workspace. This removes the send from Watchatic history/detail views; it does not revoke or delete WhatsApp messages from recipient chats.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OutboundSendIdPath"
      responses:
        "202":
          description: Send delete event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid send id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Outbound send was not found for the authenticated workspace.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/outbound/drafts:
    get:
      operationId: listOutboundDrafts
      tags:
        - outbound
      summary: List the caller's outbound send drafts
      description: Returns user-scoped composer drafts for the authenticated workspace. Drafts are isolated by actor user id; workspace admins do not see other users' drafts.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OutboundSessionIdQuery"
      responses:
        "200":
          description: User-scoped outbound send drafts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutboundSendDraftPage"
        "400":
          description: Invalid query.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: saveOutboundDraft
      tags:
        - outbound
      summary: Save the caller's outbound send draft
      description: Creates a new caller-owned draft for the selected sending session. This endpoint never sends, schedules, or enqueues WhatsApp messages.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOutboundSendDraftRequest"
      responses:
        "200":
          description: Draft saved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutboundSendDraft"
        "400":
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Sending session was not found for the authenticated workspace.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/outbound/drafts/{draft_id}:
    get:
      operationId: getOutboundDraft
      tags:
        - outbound
      summary: Get one outbound send draft
      description: Reads a single draft owned by the authenticated actor user.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OutboundDraftIdPath"
      responses:
        "200":
          description: Outbound send draft.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutboundSendDraft"
        "400":
          description: Invalid draft id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Draft was not found for the authenticated actor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      operationId: deleteOutboundDraft
      tags:
        - outbound
      summary: Delete one outbound send draft
      description: Deletes a draft owned by the authenticated actor user.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OutboundDraftIdPath"
      responses:
        "204":
          description: Draft deleted.
        "400":
          description: Invalid draft id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Draft was not found for the authenticated actor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/outbound/group-presets:
    get:
      operationId: listOutboundGroupPresets
      tags:
        - outbound
      summary: List the caller's outbound group presets
      description: Returns reusable, actor-scoped group recipient presets for the authenticated workspace.
      security:
        - BearerAuth: []
      responses:
        "200":
          description: User-scoped outbound group presets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutboundGroupPresetPage"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: createOutboundGroupPreset
      tags:
        - outbound
      summary: Create an outbound group preset
      description: Saves a named set of internal group ids owned by the authenticated actor. Group ids must belong to the workspace and be visible to the actor.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOutboundGroupPresetRequest"
      responses:
        "201":
          description: Group preset created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutboundGroupPreset"
        "400":
          description: Invalid preset request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: A preset with this name already exists for the actor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/outbound/group-presets/{preset_id}:
    patch:
      operationId: patchOutboundGroupPreset
      tags:
        - outbound
      summary: Update an outbound group preset
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OutboundPresetIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchOutboundGroupPresetRequest"
      responses:
        "200":
          description: Group preset updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutboundGroupPreset"
        "400":
          description: Invalid preset request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Preset was not found for the authenticated actor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: A preset with this name already exists for the actor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      operationId: deleteOutboundGroupPreset
      tags:
        - outbound
      summary: Delete an outbound group preset
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OutboundPresetIdPath"
      responses:
        "204":
          description: Group preset deleted.
        "400":
          description: Invalid preset id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Preset was not found for the authenticated actor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/outbound/schedules:
    get:
      operationId: listOutboundSchedules
      tags:
        - outbound
      summary: List scheduled outbound WhatsApp sends
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OutboundSessionIdQuery"
        - $ref: "#/components/parameters/OutboundStatusQuery"
        - $ref: "#/components/parameters/CursorQuery"
        - $ref: "#/components/parameters/PageSizeQuery"
      responses:
        "200":
          description: Cursor page of outbound schedules.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutboundTextSendSchedulePage"
        "400":
          description: Invalid query.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/outbound/schedules/{schedule_id}:
    get:
      operationId: getOutboundSchedule
      tags:
        - outbound
      summary: Get scheduled outbound WhatsApp send detail
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OutboundScheduleIdPath"
      responses:
        "200":
          description: Outbound schedule detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutboundTextSendSchedule"
        "400":
          description: Invalid schedule id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Outbound schedule was not found for the authenticated workspace.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      operationId: cancelOutboundSchedule
      tags:
        - outbound
      summary: Cancel a scheduled outbound WhatsApp send
      description: Cancels the Temporal workflow when it exists and appends a schedule-canceled event for projection.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/OutboundScheduleIdPath"
      responses:
        "202":
          description: Schedule cancel command accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancelOutboundScheduleResponse"
        "400":
          description: Invalid schedule id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Outbound schedule was not found for the authenticated workspace.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Schedule is already terminal and cannot be canceled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/messages/{canonical_message_id}:
    get:
      operationId: getMessage
      tags:
        - messages
      summary: Get projected canonical message detail
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/CanonicalMessageIdPath"
      responses:
        "200":
          description: Canonical message detail with bounded occurrence and media context.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageDetail"
        "400":
          description: Invalid message id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Message was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/messages/{canonical_message_id}/occurrences:
    get:
      operationId: listMessageOccurrences
      tags:
        - messages
      summary: List WAHA occurrences for one canonical message
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/CanonicalMessageIdPath"
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: Cursor page of WAHA message occurrences.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OccurrencePage"
        "400":
          description: Invalid query or message id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Message was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/messages/{canonical_message_id}/occurrences/summary:
    get:
      operationId: getMessageOccurrenceSummary
      tags:
        - messages
      summary: Summarize WAHA occurrences for one canonical message
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/CanonicalMessageIdPath"
      responses:
        "200":
          description: Exact day and group counts for one canonical message.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OccurrenceSummary"
        "400":
          description: Invalid message id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Message was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/listings/search:
    post:
      operationId: searchMarketListings
      tags:
        - market
      summary: Search projected market listings
      description: Flexible cursor search over projected classified watch listings. Saved view filters and request filters are combined with AND semantics.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MarketSearchRequest"
      responses:
        "200":
          description: Cursor page of market listings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketListingPage"
        "400":
          description: Invalid market search request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Saved view was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/live-feed/search:
    post:
      operationId: searchMarketLiveDealFeed
      tags:
        - market
      summary: Search live cheap-listing feed
      description: Returns newly extracted for-sale watch listings that are at least the requested discount below the median comp price for the same effective saved-view and watch filter set. The request must resolve to exactly one reference filter; market scope such as EU should be supplied through view_id.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LiveDealFeedRequest"
      responses:
        "200":
          description: Cheap live listing feed for the effective filter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LiveDealFeedResponse"
        "400":
          description: Invalid live feed request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Saved view was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/listings/{listing_id}:
    get:
      operationId: getMarketListing
      tags:
        - market
      summary: Get one projected market listing
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/MarketListingIdPath"
        - name: display_currency
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
          example: USD
      responses:
        "200":
          description: Market listing detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketListingItem"
        "400":
          description: Invalid listing id or currency.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Listing was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/brands/rolex/official-vs-market:
    post:
      operationId: rolexOfficialVsMarket
      tags:
        - market
      summary: Compare official Rolex list prices with market averages
      description: Returns official Rolex reference/configuration prices joined with market averages from projected listings. No external Rolex calls are made in the API request path.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OfficialVsMarketRequest"
      responses:
        "200":
          description: Official Rolex prices joined with market aggregates.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OfficialVsMarketResponse"
        "400":
          description: Invalid calculator request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Saved view was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Calculator is only available to the hardcoded allowed user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/brands/patek/official-vs-market:
    post:
      operationId: patekOfficialVsMarket
      tags:
        - market
      summary: Compare official Patek list prices with market averages
      description: Returns official Patek Philippe reference prices joined with market averages from projected listings. No external Patek calls are made in the API request path.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OfficialVsMarketRequest"
      responses:
        "200":
          description: Official Patek prices joined with market aggregates.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OfficialVsMarketResponse"
        "400":
          description: Invalid calculator request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Saved view was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Calculator is only available to the hardcoded allowed user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/brands/rolex/official-configurations:
    get:
      operationId: rolexOfficialConfigurations
      tags:
        - market
      summary: List official Rolex configurations for calculator rows
      security:
        - BearerAuth: []
      parameters:
        - name: reference
          in: query
          required: false
          schema:
            type: string
          example: 126610LN
      responses:
        "200":
          description: Official Rolex configurations and default calculator mapping state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OfficialConfigurationOptionsResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Calculator is only available to the hardcoded allowed user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/brands/patek/official-configurations:
    get:
      operationId: patekOfficialConfigurations
      tags:
        - market
      summary: List official Patek references as calculator options
      security:
        - BearerAuth: []
      parameters:
        - name: reference
          in: query
          required: false
          schema:
            type: string
          example: 5711/1A-010
      responses:
        "200":
          description: Official Patek references and default calculator mapping state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OfficialConfigurationOptionsResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Calculator is only available to the hardcoded allowed user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/calculator/filter-mappings:
    get:
      operationId: listCalculatorFilterMappings
      tags:
        - market
      summary: List saved calculator filter mappings
      security:
        - BearerAuth: []
      parameters:
        - name: brand
          in: query
          required: false
          schema:
            type: string
        - name: reference
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Saved calculator mappings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalculatorFilterMappingPage"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Calculator is only available to the hardcoded allowed user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    put:
      operationId: upsertCalculatorFilterMapping
      tags:
        - market
      summary: Create or update a calculator default filter mapping
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpsertCalculatorFilterMappingRequest"
      responses:
        "202":
          description: Mapping upsert event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid calculator mapping command.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Calculator is only available to the hardcoded allowed user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/calculator/filter-mappings/{mapping_id}:
    delete:
      operationId: deleteCalculatorFilterMapping
      tags:
        - market
      summary: Delete a calculator filter mapping
      security:
        - BearerAuth: []
      parameters:
        - name: mapping_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "202":
          description: Mapping deletion event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Calculator is only available to the hardcoded allowed user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/calculator/presets:
    get:
      operationId: listCalculatorPresets
      tags:
        - market
      summary: List saved calculator presets
      security:
        - BearerAuth: []
      parameters:
        - name: brand
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Saved calculator presets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalculatorPresetPage"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Calculator is only available to the hardcoded allowed user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: createCalculatorPreset
      tags:
        - market
      summary: Create a saved calculator preset
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCalculatorPresetRequest"
      responses:
        "202":
          description: Preset creation event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid calculator preset command.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Calculator is only available to the hardcoded allowed user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/calculator/presets/{preset_id}:
    patch:
      operationId: patchCalculatorPreset
      tags:
        - market
      summary: Update a saved calculator preset
      security:
        - BearerAuth: []
      parameters:
        - name: preset_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchCalculatorPresetRequest"
      responses:
        "202":
          description: Preset update event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid calculator preset command.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Calculator is only available to the hardcoded allowed user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Preset not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      operationId: deleteCalculatorPreset
      tags:
        - market
      summary: Delete a saved calculator preset
      security:
        - BearerAuth: []
      parameters:
        - name: preset_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "202":
          description: Preset deletion event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Calculator is only available to the hardcoded allowed user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Preset not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/references/search:
    post:
      operationId: searchMarketReferences
      tags:
        - market
      summary: Search market reference aggregates
      description: Returns reference-level aggregates and facets from the same listing filters used by listing search.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MarketSearchRequest"
      responses:
        "200":
          description: Cursor page of reference aggregates.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketReferencePage"
        "400":
          description: Invalid market reference search request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/summary:
    post:
      operationId: summarizeMarket
      tags:
        - market
      summary: Summarize market listings for a filter
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MarketSearchRequest"
      responses:
        "200":
          description: Market listing summary for the effective filter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketSummary"
        "400":
          description: Invalid market summary request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/trends:
    post:
      operationId: analyzeMarketTrend
      tags:
        - market
      summary: Analyze bucketed market trend movement
      description: Returns bucketed price or listing-count points with sample counts, confidence and warnings for trend claims.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MarketTrendRequest"
      responses:
        "200":
          description: Bucketed trend response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketTrendResponse"
        "400":
          description: Invalid trend request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/market/facets:
    post:
      operationId: getMarketFacets
      tags:
        - market
      summary: Get cross-filtered market facets
      description: Returns facet buckets for the effective listing filter. Each facet omits its own field so the UI can show available alternatives.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MarketSearchRequest"
      responses:
        "200":
          description: Cross-filtered market facets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketFacetSet"
        "400":
          description: Invalid market facet request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/alerts:
    get:
      operationId: listAlerts
      tags:
        - alerts
      summary: List market alerts
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Alert list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertPage"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: createAlert
      tags:
        - alerts
      summary: Create a market alert
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAlertRequest"
      responses:
        "202":
          description: Alert creation event accepted. The read model is updated asynchronously by the alert projector.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid alert request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/alerts/matches/seen:
    post:
      operationId: markAllAlertMatchesSeen
      tags:
        - alerts
      summary: Mark all visible unread alert matches as seen
      description: Marks every currently unread alert match visible to the authenticated workspace user as seen. Workspace member session access is honored before matches are updated.
      security:
        - BearerAuth: []
      responses:
        "202":
          description: Bulk alert match seen events accepted and the alert match read model was updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertBulkMarkSeenResult"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/alerts/{alert_id}:
    get:
      operationId: getAlert
      tags:
        - alerts
      summary: Get one market alert
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AlertIdPath"
      responses:
        "200":
          description: Alert detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertItem"
        "404":
          description: Alert not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    patch:
      operationId: updateAlert
      tags:
        - alerts
      summary: Update one market alert
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AlertIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateAlertRequest"
      responses:
        "202":
          description: Alert update event accepted. The read model is updated asynchronously by the alert projector.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid alert request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Alert not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      operationId: deleteAlert
      tags:
        - alerts
      summary: Delete one market alert
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AlertIdPath"
      responses:
        "202":
          description: Alert delete event accepted. The read model is updated asynchronously by the alert projector.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "404":
          description: Alert not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/alerts/{alert_id}/matches:
    get:
      operationId: listAlertMatches
      tags:
        - alerts
      summary: List alert matches
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AlertIdPath"
        - name: unseen_only
          in: query
          required: false
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: Alert match list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertMatchPage"
  /v2/alerts/{alert_id}/matches/{listing_id}/seen:
    post:
      operationId: markAlertMatchSeen
      tags:
        - alerts
      summary: Mark an alert match as seen
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AlertIdPath"
        - $ref: "#/components/parameters/MarketListingIdPath"
      responses:
        "202":
          description: Alert match seen event accepted. The read model is updated asynchronously by the alert projector.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
  /v2/dashboard/stats:
    get:
      operationId: getDashboardStats
      tags:
        - dashboard
      summary: Get dashboard aggregate stats
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Dashboard stats.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardStats"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/dashboard/recently-viewed:
    get:
      operationId: listDashboardRecentlyViewed
      tags:
        - dashboard
      summary: List recently viewed command palette items
      description: Returns the user's recently viewed workspace items. Until server-side history is stored, new users receive an empty item list.
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Recently viewed items.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecentlyViewedResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/search/history:
    get:
      operationId: listSearchHistory
      tags:
        - search
      summary: List recent account search queries
      description: Returns the last five submitted search queries for the authenticated Clerk user, independent of the active workspace.
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Recent search queries.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchHistoryResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: recordSearchHistory
      tags:
        - search
      summary: Record an account search query
      description: Upserts a submitted search query for the authenticated Clerk user and trims the account history to the latest five entries.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RecordSearchHistoryRequest"
      responses:
        "200":
          description: Updated recent search queries.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchHistoryResponse"
        "400":
          description: Invalid search query.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/agent/threads:
    get:
      operationId: listAgentThreads
      tags:
        - agent
      summary: List persisted assistant threads
      description: Lists assistant threads stored at the authenticated Clerk user level, not the workspace level.
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Persisted assistant threads.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentThreadListResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: initializeAgentThread
      tags:
        - agent
      summary: Initialize a persisted assistant thread
      description: Creates a thread row for the authenticated Clerk user, or returns the existing row when the remote id is already present for that user.
      security:
        - BearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InitializeAgentThreadRequest"
      responses:
        "200":
          description: Initialized thread id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InitializeAgentThreadResponse"
        "400":
          description: Invalid thread id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/agent/threads/{thread_id}:
    parameters:
      - $ref: "#/components/parameters/AgentThreadIdPath"
    get:
      operationId: getAgentThread
      tags:
        - agent
      summary: Get persisted assistant thread metadata
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Thread metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentThreadSummary"
        "404":
          description: Thread not found for the authenticated user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    patch:
      operationId: patchAgentThread
      tags:
        - agent
      summary: Update persisted assistant thread metadata
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchAgentThreadRequest"
      responses:
        "200":
          description: Updated thread metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentThreadSummary"
        "400":
          description: Invalid metadata patch.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Thread not found for the authenticated user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      operationId: deleteAgentThread
      tags:
        - agent
      summary: Delete a persisted assistant thread
      security:
        - BearerAuth: []
      responses:
        "204":
          description: Thread deleted.
        "404":
          description: Thread not found for the authenticated user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/agent/threads/{thread_id}/history:
    parameters:
      - $ref: "#/components/parameters/AgentThreadIdPath"
    get:
      operationId: getAgentThreadHistory
      tags:
        - agent
      summary: Get persisted assistant thread messages
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Assistant UI thread history.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentThreadHistory"
        "404":
          description: Thread not found for the authenticated user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    put:
      operationId: putAgentThreadHistory
      tags:
        - agent
      summary: Replace persisted assistant thread messages
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentThreadHistory"
      responses:
        "200":
          description: Stored Assistant UI thread history.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentThreadHistory"
        "400":
          description: Invalid thread history.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Thread not found for the authenticated user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/ai/model-profiles:
    get:
      operationId: listAiModelProfiles
      tags:
        - ai
      summary: List available classification model profiles and connector key status
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Model profiles and current tenant classification settings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelProfilesResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/ai/connectors/{connector_id}/key:
    put:
      operationId: storeAiConnectorKey
      tags:
        - ai
      summary: Store an encrypted AI connector key for classification
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ConnectorIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConnectorKeyRequest"
      responses:
        "202":
          description: Key storage event accepted. The raw key is never returned or stored in events.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid connector id or missing encryption configuration.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      operationId: removeAiConnectorKey
      tags:
        - ai
      summary: Remove an AI connector key
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ConnectorIdPath"
      responses:
        "202":
          description: Key removal event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid connector id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/classification-settings:
    get:
      operationId: getClassificationSettings
      tags:
        - classifications
      summary: Get current classification model settings
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Current classification settings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClassificationSettings"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    put:
      operationId: updateClassificationSettings
      tags:
        - classifications
      summary: Select the classification model profile
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateClassificationSettingsRequest"
      responses:
        "202":
          description: Settings update event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid model profile id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/actions:
    get:
      operationId: listActions
      tags:
        - actions
      summary: List my reversible v2 API actions
      description: Returns only action history entries created by the authenticated actor in the current tenant or workspace scope.
      security:
        - BearerAuth: []
      parameters:
        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 30
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: category
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/ActionCategory"
        - name: revert_status
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/ActionRevertStatus"
      responses:
        "200":
          description: Cursor page of actor-scoped action history entries.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionPage"
        "400":
          description: Invalid action history filter or cursor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/actions/{action_id}/revert/preview:
    post:
      operationId: previewActionRevert
      tags:
        - actions
      summary: Preview undo for one action
      description: Computes the compensating event plan without mutating state. Conflicts indicate later API events on the same primary stream.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ActionIdPath"
      responses:
        "200":
          description: Undo preview including effects, conflicts and expected stream versions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionRevertPreview"
        "400":
          description: Invalid action id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Action was not found for the authenticated actor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Action is not undoable.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/actions/{action_id}/revert:
    post:
      operationId: revertAction
      tags:
        - actions
      summary: Confirm undo for one action
      description: Appends compensating events for the previewed action. The request must carry the expected_versions from a recent preview.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ActionIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ActionRevertRequest"
      responses:
        "202":
          description: Compensating event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid action id or request body.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Action was not found for the authenticated actor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: The action is irreversible, already reverted, or a stream version changed since preview.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/entity-logs/{entity_type}/{entity_id}:
    get:
      operationId: listEntityLogs
      tags:
        - logs
      summary: List paginated activity logs for one entity
      description: Returns a tenant-scoped entity timeline. `watch_listing` is the public watch-level entity used by market cards; tenant singleton settings such as classification AI settings use `entity_id=current`.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/EntityLogEntityTypePath"
        - $ref: "#/components/parameters/EntityLogEntityIdPath"
        - name: scope
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/EntityLogScope"
        - name: category
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/EntityLogCategory"
        - name: from_ms
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: to_ms
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: Cursor page of entity log entries.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EntityLogPage"
        "400":
          description: Invalid entity type, id or cursor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Entity was not found for the authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/classifications:
    get:
      operationId: listClassifications
      tags:
        - classifications
      summary: List completed message classifications
      security:
        - BearerAuth: []
      parameters:
        - name: message_type
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/MessageIntent"
        - name: canonical_message_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: Cursor page of classifications.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClassificationPage"
        "400":
          description: Invalid query.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/classifications/{classification_id}:
    get:
      operationId: getClassification
      tags:
        - classifications
      summary: Get one classification result
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ClassificationIdPath"
      responses:
        "200":
          description: Classification detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClassificationItem"
        "400":
          description: Invalid classification id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Classification was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/classifications/{classification_id}/corrections:
    post:
      operationId: correctClassification
      tags:
        - classifications
      summary: Append a user correction for a classification
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ClassificationIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CorrectClassificationRequest"
      responses:
        "202":
          description: Correction event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid correction payload.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Classification was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/messages/{canonical_message_id}/classifications/retry:
    post:
      operationId: retryMessageClassification
      tags:
        - classifications
      summary: Request classification retry for a canonical message
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/CanonicalMessageIdPath"
      responses:
        "202":
          description: Classification retry request event accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommandResult"
        "400":
          description: Invalid message id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Message was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/profiles/contacts:
    get:
      operationId: listContacts
      tags:
        - profiles
      summary: List contact profiles surfaced from WhatsApp groups
      description: Cursor-paginated list of contact identities with aggregated activity, listing and group counts.
      security:
        - BearerAuth: []
      parameters:
        - name: q
          in: query
          required: false
          schema:
            type: string
          description: Free-text search over display name, phone number, LID and raw sender id.
        - name: country_prefix
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated list of country prefixes to include.
        - name: country_prefix_exclude
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated list of country prefixes to exclude.
        - name: has_selling
          in: query
          required: false
          schema:
            type: boolean
          description: Restrict to contacts with at least one for_sale listing.
        - name: has_buying
          in: query
          required: false
          schema:
            type: boolean
          description: Restrict to contacts with at least one want_to_buy listing.
        - name: group_id
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated list of group UUIDs to scope contacts to.
        - name: last_seen_from_ms
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: last_seen_to_ms
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: sort
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/ContactSortField"
        - name: direction
          in: query
          required: false
          schema:
            type: string
            enum: [asc, desc]
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: Cursor page of contacts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContactPage"
        "400":
          description: Invalid query.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/profiles/contacts/search:
    post:
      operationId: searchContacts
      tags:
        - profiles
      summary: Search contact profiles with structured filters
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContactSearchRequest"
      responses:
        "200":
          description: Cursor page of contacts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContactPage"
        "400":
          description: Invalid query.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /v2/profiles/contacts/{contact_id}:
    get:
      operationId: getContact
      tags:
        - profiles
      summary: Get contact profile detail
      security:
        - BearerAuth: []
      parameters:
        - name: contact_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Contact detail with aggregations.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContactDetail"
        "400":
          description: Invalid contact id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid Clerk bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Contact was not found or is not visible in this scope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    WahaSecret:
      type: apiKey
      in: header
      name: X-WAHA-SECRET
    SvixSignature:
      type: apiKey
      in: header
      name: Svix-Signature
  parameters:
    GroupIdPath:
      name: group_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    CanonicalMessageIdPath:
      name: canonical_message_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ClassificationIdPath:
      name: classification_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ConnectorIdPath:
      name: connector_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/AIConnectorId"
    ViewIdPath:
      name: view_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    AlertIdPath:
      name: alert_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ActionIdPath:
      name: action_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    MarketListingIdPath:
      name: listing_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    AgentThreadIdPath:
      name: thread_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    EntityLogEntityTypePath:
      name: entity_type
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/EntityLogEntityType"
    EntityLogEntityIdPath:
      name: entity_id
      in: path
      required: true
      schema:
        type: string
        minLength: 1
      example: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7070
    OutboundSendIdPath:
      name: send_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    OutboundScheduleIdPath:
      name: schedule_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    OutboundDraftIdPath:
      name: draft_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    OutboundPresetIdPath:
      name: preset_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    OutboundSessionIdQuery:
      name: session_id
      in: query
      required: false
      schema:
        type: string
        format: uuid
    OutboundStatusQuery:
      name: status
      in: query
      required: false
      schema:
        type: string
      example: queued
    CursorQuery:
      name: cursor
      in: query
      required: false
      schema:
        type: string
    PageSizeQuery:
      name: page_size
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
  schemas:
    CommandResult:
      type: object
      required:
        - status
        - event_ids
        - request_id
      properties:
        status:
          type: string
          enum:
            - accepted
            - duplicate_accepted
            - rejected
            - conflict
            - noop
        event_ids:
          type: array
          items:
            type: string
            format: uuid
        resource_id:
          type: string
          nullable: true
        request_id:
          type: string
      example:
        status: accepted
        event_ids:
          - 018f06e2-4f8d-7c01-8a0d-cc6fd78f7002
        request_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7001
    ErrorBody:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
        message:
          type: string
        request_id:
          type: string
      example:
        code: invalid_payload
        message: event is required
        request_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7001
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          $ref: "#/components/schemas/ErrorBody"
    ContactSortField:
      type: string
      enum:
        - last_seen
        - listings
        - messages
        - references
      default: last_seen
    ContactSummary:
      type: object
      required:
        - id
        - raw_sender_id
        - resolution_status
        - first_seen_at_ms
        - last_seen_at_ms
        - message_count
        - classified_count
        - listing_count
        - selling_count
        - buying_count
        - reference_count
        - group_count
      properties:
        id:
          type: string
          format: uuid
        raw_sender_id:
          type: string
        display_name:
          type: string
          nullable: true
        phone_number:
          type: string
          nullable: true
        lid:
          type: string
          nullable: true
        country_prefix:
          type: string
          nullable: true
        resolution_status:
          type: string
        first_seen_at_ms:
          type: integer
          format: int64
        last_seen_at_ms:
          type: integer
          format: int64
        message_count:
          type: integer
          format: int64
        classified_count:
          type: integer
          format: int64
        listing_count:
          type: integer
          format: int64
        selling_count:
          type: integer
          format: int64
        buying_count:
          type: integer
          format: int64
        reference_count:
          type: integer
          format: int64
        group_count:
          type: integer
          format: int64
    ContactPage:
      type: object
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ContactSummary"
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
    ContactIdentityHistoryItem:
      type: object
      required:
        - kind
        - value
        - first_seen_at_ms
        - last_seen_at_ms
        - status
      properties:
        kind:
          type: string
          example: phone_number
        value:
          type: string
        display_name:
          type: string
          nullable: true
        country_prefix:
          type: string
          nullable: true
        first_seen_at_ms:
          type: integer
          format: int64
        last_seen_at_ms:
          type: integer
          format: int64
        status:
          type: string
    ContactGroupMembership:
      type: object
      required:
        - group_id
        - group_name
        - message_count
        - last_seen_at_ms
      properties:
        group_id:
          type: string
          format: uuid
        group_name:
          type: string
        message_count:
          type: integer
          format: int64
        last_seen_at_ms:
          type: integer
          format: int64
    ContactTopReference:
      type: object
      required:
        - brand
        - reference
        - listing_count
        - selling_count
        - buying_count
      properties:
        brand:
          type: string
        reference:
          type: string
        listing_count:
          type: integer
          format: int64
        selling_count:
          type: integer
          format: int64
        buying_count:
          type: integer
          format: int64
    ContactActivityBucket:
      type: object
      required:
        - day_ms
        - message_count
      properties:
        day_ms:
          type: integer
          format: int64
        message_count:
          type: integer
          format: int64
    ContactDetail:
      allOf:
        - $ref: "#/components/schemas/ContactSummary"
        - type: object
          required:
            - identity_history
            - group_memberships
            - top_references
            - activity_buckets
            - activity_range_days
          properties:
            identity_history:
              type: array
              items:
                $ref: "#/components/schemas/ContactIdentityHistoryItem"
            group_memberships:
              type: array
              items:
                $ref: "#/components/schemas/ContactGroupMembership"
            top_references:
              type: array
              items:
                $ref: "#/components/schemas/ContactTopReference"
            activity_buckets:
              type: array
              items:
                $ref: "#/components/schemas/ContactActivityBucket"
            activity_range_days:
              type: integer
    ContactSearchRequest:
      type: object
      additionalProperties: false
      properties:
        q:
          type: string
        country_prefixes_include:
          type: array
          items:
            type: string
        country_prefixes_exclude:
          type: array
          items:
            type: string
        has_selling:
          type: boolean
        has_buying:
          type: boolean
        group_ids:
          type: array
          items:
            type: string
            format: uuid
        last_seen_from_ms:
          type: integer
          format: int64
        last_seen_to_ms:
          type: integer
          format: int64
        sort:
          $ref: "#/components/schemas/ContactSortField"
        direction:
          type: string
          enum: [asc, desc]
        page_size:
          type: integer
          minimum: 1
          maximum: 200
        cursor:
          type: string
    SendTextMessageRequest:
      type: object
      required:
        - session_id
        - group_ids
        - idempotency_key
      properties:
        session_id:
          type: string
          format: uuid
        group_ids:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
            format: uuid
        text:
          type: string
          maxLength: 4096
          description: Text body for a text send, or image caption when media is present. Required when media is omitted.
        media:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/OutboundMedia"
          description: Uploaded image media to send with the message. At least text or media must be provided.
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
      example:
        session_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7070
        group_ids:
          - 018f06e2-4f8d-7c01-8a0d-cc6fd78f7300
        text: Interested in this watch?
        idempotency_key: frontend-send-018f06e2
    SendTextMessageRecipientResult:
      type: object
      required:
        - recipient_send_id
        - group_id
        - ok
        - status
      properties:
        recipient_send_id:
          type: string
          format: uuid
        group_id:
          type: string
          format: uuid
        ok:
          type: boolean
        status:
          type: string
          enum:
            - queued
            - sent
            - failed
        waha_message_id:
          type: string
          nullable: true
        error_code:
          type: string
          nullable: true
      example:
        recipient_send_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7101
        group_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7300
        ok: true
        status: queued
    SendTextMessageResponse:
      type: object
      required:
        - status
        - send_id
        - results
        - request_id
      properties:
        status:
          type: string
          enum:
            - accepted
            - duplicate_accepted
        send_id:
          type: string
          format: uuid
        event_ids:
          type: array
          items:
            type: string
            format: uuid
        results:
          type: array
          items:
            $ref: "#/components/schemas/SendTextMessageRecipientResult"
        request_id:
          type: string
      example:
        status: accepted
        send_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7100
        event_ids:
          - 018f06e2-4f8d-7c01-8a0d-cc6fd78f7102
        results:
          - recipient_send_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7101
            group_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7300
            ok: true
            status: queued
        request_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7001
    OutboundSendSchedule:
      type: object
      required:
        - first_run_at_ms
      properties:
        first_run_at_ms:
          type: integer
          format: int64
          description: First dispatch time as Unix milliseconds. Must be in the future.
        interval_ms:
          type: integer
          format: int64
          nullable: true
          minimum: 60000
          description: Repeat interval in milliseconds. Required when max_occurrences is greater than 1.
        max_occurrences:
          type: integer
          minimum: 1
          maximum: 365
          default: 1
          description: Total number of occurrences Temporal should dispatch.
      additionalProperties: false
      example:
        first_run_at_ms: 1777809600000
        interval_ms: 3600000
        max_occurrences: 3
    OutboundMedia:
      type: object
      required:
        - url
        - mimetype
      properties:
        url:
          type: string
          format: uri
          description: Public HTTP(S) URL from a trusted image source. Multipart file uploads on `/v2/outbound/sends` are stored before the send is queued and normalized to this shape internally.
        mimetype:
          type: string
          enum:
            - image/jpeg
            - image/png
            - image/gif
            - image/webp
        filename:
          type: string
        sha256:
          type: string
        size_bytes:
          type: integer
          format: int64
        storage_key:
          type: string
      additionalProperties: false
      example:
        url: https://watchatic-media.example/outbound/org_123/watch.jpg
        mimetype: image/jpeg
        filename: watch.jpg
        sha256: 9a0364b9e99bb480dd25e1f0284c8555
        size_bytes: 238991
        storage_key: outbound/org_123/20260511T025700.000-watch.jpg
    CreateOutboundSendRequest:
      type: object
      required:
        - session_id
        - group_ids
        - idempotency_key
      properties:
        session_id:
          type: string
          format: uuid
          description: WORKING sending session id. Receiving sessions cannot be used for outbound sends.
        group_ids:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
            format: uuid
          description: Internal group ids from `/v2/groups` that are present in the selected sending session.
        text:
          type: string
          maxLength: 4096
          description: Text body for a text send, or image caption when media is present. Required when media is omitted.
        media:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/OutboundMedia"
          description: Image media URLs to send with the message. At least text, media or multipart files must be provided.
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
        schedule:
          $ref: "#/components/schemas/OutboundSendSchedule"
      additionalProperties: false
      example:
        session_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7070
        group_ids:
          - 018f06e2-4f8d-7c01-8a0d-cc6fd78f7300
        text: Interested in this watch?
        media:
          - url: https://watchatic-media.example/outbound/org_123/watch.jpg
            mimetype: image/jpeg
            filename: watch.jpg
        idempotency_key: frontend-outbound-018f06e2
        schedule:
          first_run_at_ms: 1777809600000
          interval_ms: 86400000
          max_occurrences: 5
    CreateOutboundSendDraftRequest:
      type: object
      required:
        - session_id
      properties:
        session_id:
          type: string
          format: uuid
          description: Sending session id this draft belongs to. A caller can create multiple drafts for the same sending session.
        group_ids:
          type: array
          maxItems: 100
          items:
            type: string
            format: uuid
          description: Internal group ids from `/v2/groups` that are present in the selected sending session. May be empty while composing.
        text:
          type: string
          maxLength: 4096
          description: Draft text body, or image caption when media is present.
        media:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/OutboundMedia"
          description: Already-uploaded image media URLs to keep with the draft. Multipart local files are not accepted by draft saves.
        schedule:
          $ref: "#/components/schemas/OutboundSendSchedule"
      additionalProperties: false
      example:
        session_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7070
        group_ids:
          - 018f06e2-4f8d-7c01-8a0d-cc6fd78f7300
        text: Interested in this watch?
        media:
          - url: https://watchatic-media.example/outbound/org_123/watch.jpg
            mimetype: image/jpeg
            filename: watch.jpg
    OutboundSendDraft:
      type: object
      required:
        - id
        - actor_user_id
        - session_id
        - session_name
        - session_label
        - group_ids
        - text
        - media
        - created_at_ms
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        actor_user_id:
          type: string
          description: Clerk user id that owns this draft. Only this actor can read or delete it.
        session_id:
          type: string
          format: uuid
        session_name:
          type: string
        session_label:
          type: string
        group_ids:
          type: array
          items:
            type: string
            format: uuid
        text:
          type: string
        media:
          type: array
          items:
            $ref: "#/components/schemas/OutboundMedia"
        schedule:
          $ref: "#/components/schemas/OutboundSendSchedule"
        created_at_ms:
          type: integer
          format: int64
        updated_at_ms:
          type: integer
          format: int64
      additionalProperties: false
    OutboundSendDraftPage:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/OutboundSendDraft"
      additionalProperties: false
    CreateOutboundGroupPresetRequest:
      type: object
      required:
        - name
        - group_ids
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
          description: Human-readable preset name scoped to the actor.
        description:
          type: string
          nullable: true
          maxLength: 500
        group_ids:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
            format: uuid
          description: Internal group ids from `/v2/groups`. They must belong to the authenticated workspace.
      additionalProperties: false
      example:
        name: EU dealer groups
        description: Daily outbound broadcast set
        group_ids:
          - 018f06e2-4f8d-7c01-8a0d-cc6fd78f7300
    PatchOutboundGroupPresetRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type: string
          nullable: true
          maxLength: 500
        group_ids:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
            format: uuid
      additionalProperties: false
      example:
        name: VIP dealer groups
        group_ids:
          - 018f06e2-4f8d-7c01-8a0d-cc6fd78f7300
    OutboundGroupPreset:
      type: object
      required:
        - id
        - actor_user_id
        - name
        - group_ids
        - created_at_ms
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        actor_user_id:
          type: string
          description: Clerk user id that owns this preset.
        name:
          type: string
        description:
          type: string
          nullable: true
        group_ids:
          type: array
          items:
            type: string
            format: uuid
        created_at_ms:
          type: integer
          format: int64
        updated_at_ms:
          type: integer
          format: int64
      additionalProperties: false
    OutboundGroupPresetPage:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/OutboundGroupPreset"
      additionalProperties: false
    CreateOutboundSendMultipartRequest:
      type: object
      required:
        - session_id
        - group_ids
        - idempotency_key
      properties:
        session_id:
          type: string
          format: uuid
          description: WORKING sending session id. Receiving sessions cannot be used for outbound sends.
        group_ids:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
            format: uuid
          description: Internal group ids from `/v2/groups` that are present in the selected sending session. Send as repeated form fields, a comma-separated value or a JSON array string.
        text:
          type: string
          maxLength: 4096
          description: Text body for a text send, or image caption when files or media are present. Required when files and media are omitted.
        files:
          type: array
          maxItems: 10
          items:
            type: string
            format: binary
          description: JPEG, PNG, GIF or WebP images up to 10MB each. These replace the removed standalone outbound media upload endpoint.
        media:
          type: string
          description: Optional JSON-encoded OutboundMedia object or array for trusted image URLs.
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
        schedule:
          type: string
          description: Optional JSON-encoded OutboundSendSchedule.
      additionalProperties: false
      example:
        session_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7070
        group_ids:
          - 018f06e2-4f8d-7c01-8a0d-cc6fd78f7300
        text: Interested in this watch?
        idempotency_key: frontend-outbound-018f06e2
    OutboundScheduledSendResult:
      type: object
      required:
        - status
        - schedule_id
        - next_run_at_ms
        - max_occurrences
      properties:
        status:
          type: string
          enum:
            - accepted
            - duplicate_accepted
        schedule_id:
          type: string
          format: uuid
        workflow_id:
          type: string
        workflow_run_id:
          type: string
        next_run_at_ms:
          type: integer
          format: int64
        interval_ms:
          type: integer
          format: int64
          nullable: true
        max_occurrences:
          type: integer
        event_ids:
          type: array
          items:
            type: string
            format: uuid
        request_id:
          type: string
      additionalProperties: false
    CreateOutboundSendResponse:
      type: object
      required:
        - status
        - request_id
      properties:
        status:
          type: string
          enum:
            - accepted
            - duplicate_accepted
        send:
          $ref: "#/components/schemas/SendTextMessageResponse"
        schedule:
          $ref: "#/components/schemas/OutboundScheduledSendResult"
        request_id:
          type: string
      additionalProperties: false
      example:
        status: accepted
        schedule:
          status: accepted
          schedule_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7200
          workflow_id: outbound:text-send-schedule:018f06e2-4f8d-7c01-8a0d-cc6fd78f7200
          next_run_at_ms: 1777809600000
          interval_ms: 86400000
          max_occurrences: 5
        request_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7001
    OutboundTextSendRecipient:
      type: object
      required:
        - id
        - recipient_type
        - status
        - requested_at_ms
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        group_id:
          type: string
          format: uuid
          nullable: true
        recipient_id:
          type: string
          nullable: true
        recipient_type:
          type: string
          enum:
            - group
            - contact
            - broadcast
        recipient_label:
          type: string
          nullable: true
        status:
          type: string
          enum:
            - queued
            - sent
            - failed
        waha_message_id:
          type: string
          nullable: true
        error_code:
          type: string
          nullable: true
        error_message:
          type: string
          nullable: true
        requested_at_ms:
          type: integer
          format: int64
        completed_at_ms:
          type: integer
          format: int64
          nullable: true
        failed_at_ms:
          type: integer
          format: int64
          nullable: true
        updated_at_ms:
          type: integer
          format: int64
    OutboundTextSend:
      type: object
      required:
        - id
        - actor_user_id
        - session_id
        - session_name
        - session_label
        - text_sha256
        - text_length
        - media_count
        - media_urls
        - recipient_count
        - status
        - source_type
        - requested_at_ms
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        actor_user_id:
          type: string
        session_id:
          type: string
          format: uuid
        session_name:
          type: string
        session_label:
          type: string
        text_sha256:
          type: string
        text_length:
          type: integer
        text:
          type: string
          nullable: true
        media_count:
          type: integer
        media_urls:
          type: array
          items:
            type: string
        recipient_count:
          type: integer
        status:
          type: string
          enum:
            - queued
            - sending
            - sent
            - failed
            - partial_failed
        client_idempotency_key:
          type: string
          nullable: true
        source_type:
          type: string
        requested_at_ms:
          type: integer
          format: int64
        completed_at_ms:
          type: integer
          format: int64
          nullable: true
        updated_at_ms:
          type: integer
          format: int64
        recipients:
          type: array
          items:
            $ref: "#/components/schemas/OutboundTextSendRecipient"
    OutboundTextSendPage:
      type: object
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/OutboundTextSend"
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
    OutboundTextSendScheduleRun:
      type: object
      required:
        - occurrence_number
        - status
        - updated_at_ms
      properties:
        occurrence_number:
          type: integer
        send_id:
          type: string
          format: uuid
          nullable: true
        status:
          type: string
          enum:
            - started
            - failed
        error_code:
          type: string
          nullable: true
        error_message:
          type: string
          nullable: true
        started_at_ms:
          type: integer
          format: int64
          nullable: true
        failed_at_ms:
          type: integer
          format: int64
          nullable: true
        updated_at_ms:
          type: integer
          format: int64
    OutboundTextSendSchedule:
      type: object
      required:
        - id
        - actor_user_id
        - session_id
        - session_name
        - session_label
        - group_ids
        - text_sha256
        - text_length
        - media_count
        - status
        - first_run_at_ms
        - max_occurrences
        - run_count
        - failed_run_count
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        actor_user_id:
          type: string
        session_id:
          type: string
          format: uuid
        session_name:
          type: string
        session_label:
          type: string
        group_ids:
          type: array
          items:
            type: string
            format: uuid
        text_sha256:
          type: string
        text_length:
          type: integer
        media_count:
          type: integer
        status:
          type: string
          enum:
            - scheduled
            - running
            - completed
            - completed_with_failures
            - canceled
        first_run_at_ms:
          type: integer
          format: int64
        next_run_at_ms:
          type: integer
          format: int64
          nullable: true
        interval_ms:
          type: integer
          format: int64
          nullable: true
        max_occurrences:
          type: integer
        run_count:
          type: integer
        failed_run_count:
          type: integer
        last_run_at_ms:
          type: integer
          format: int64
          nullable: true
        completed_at_ms:
          type: integer
          format: int64
          nullable: true
        canceled_at_ms:
          type: integer
          format: int64
          nullable: true
        updated_at_ms:
          type: integer
          format: int64
        runs:
          type: array
          items:
            $ref: "#/components/schemas/OutboundTextSendScheduleRun"
    OutboundTextSendSchedulePage:
      type: object
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/OutboundTextSendSchedule"
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
    CancelOutboundScheduleResponse:
      type: object
      required:
        - status
        - schedule_id
        - request_id
      properties:
        status:
          type: string
          enum:
            - accepted
            - duplicate_accepted
        schedule_id:
          type: string
          format: uuid
        event_ids:
          type: array
          items:
            type: string
            format: uuid
        request_id:
          type: string
    WorkspaceSessionAccessGrant:
      type: object
      required:
        - session_id
        - user_id
        - granted_by_user_id
        - granted_at_ms
      properties:
        session_id:
          type: string
          format: uuid
        user_id:
          type: string
        granted_by_user_id:
          type: string
        granted_at_ms:
          type: integer
          format: int64
    WorkspaceSessionAccessPage:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/WorkspaceSessionAccessGrant"
    GrantWorkspaceSessionAccessRequest:
      type: object
      required:
        - member_user_id
        - session_id
      properties:
        member_user_id:
          type: string
        session_id:
          type: string
          format: uuid
    TransferWorkspaceOwnershipRequest:
      type: object
      required:
        - to_user_id
      properties:
        from_user_id:
          type: string
          description: Defaults to the authenticated owner when omitted.
        to_user_id:
          type: string
    WorkspaceSettings:
      type: object
      required:
        - workspace_id
        - default_currency
        - default_outlier_pct
        - default_classification_filter
        - updated_at_ms
      properties:
        workspace_id:
          type: string
        default_currency:
          type: string
          minLength: 3
          maxLength: 3
          example: EUR
        default_outlier_pct:
          type: integer
          minimum: 0
          maximum: 100
        default_classification_filter:
          type: string
        updated_at_ms:
          type: integer
          format: int64
    PatchWorkspaceSettingsRequest:
      type: object
      properties:
        default_currency:
          type: string
          minLength: 3
          maxLength: 3
          example: USD
        default_outlier_pct:
          type: integer
          minimum: 0
          maximum: 100
        default_classification_filter:
          type: string
    HealthResponse:
      type: object
      required:
        - status
        - service
        - version
      properties:
        status:
          type: string
          enum:
            - ok
            - unhealthy
        service:
          type: string
        version:
          type: string
        dependencies:
          type: object
          additionalProperties:
            type: string
      example:
        status: ok
        service: watchatic-v2-api
        version: dev
        dependencies:
          database: ok
    SessionRole:
      type: string
      enum:
        - receiving
        - sending
    SessionEngine:
      type: string
      enum:
        - WEBJS
        - NOWEB
        - GOWS
    SessionAuthMethod:
      type: string
      enum:
        - qr
        - otp
    SessionSummary:
      type: object
      required:
        - id
        - waha_name
        - role
        - engine
        - status
      properties:
        id:
          type: string
          format: uuid
        label:
          type: string
          nullable: true
          example: Main receiving session
        waha_name:
          type: string
          example: user-user_35eh25qkkIVwab803vgofO1uSsl-receiving
        role:
          $ref: "#/components/schemas/SessionRole"
        engine:
          $ref: "#/components/schemas/SessionEngine"
        status:
          type: string
          example: WORKING
        phone_number:
          type: string
          nullable: true
        connecting_user_id:
          type: string
          nullable: true
        last_status_at_ms:
          type: integer
          format: int64
          nullable: true
    SessionPage:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/SessionSummary"
    ImportPersonalSessionsRequest:
      type: object
      properties:
        session_ids:
          type: array
          description: Optional personal session ids to import. When omitted, every importable personal session owned by the actor is considered.
          items:
            type: string
            format: uuid
      additionalProperties: false
    ImportedPersonalSessionItem:
      type: object
      required:
        - session_id
        - waha_session_name
        - status
      properties:
        session_id:
          type: string
          format: uuid
        waha_session_name:
          type: string
        status:
          type: string
          enum:
            - accepted
            - failed
        event_ids:
          type: array
          items:
            type: string
            format: uuid
        error_code:
          type: string
        error_message:
          type: string
      additionalProperties: false
    ImportPersonalSessionsResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ImportedPersonalSessionItem"
        request_id:
          type: string
      additionalProperties: false
    CreateSessionRequest:
      type: object
      required:
        - role
      properties:
        role:
          $ref: "#/components/schemas/SessionRole"
        engine:
          $ref: "#/components/schemas/SessionEngine"
          description: Optional WAHA engine. Receiving sessions default to WEBJS; set GOWS only for explicit no-browser receiving tests. Sending sessions must use NOWEB.
        auth_method:
          $ref: "#/components/schemas/SessionAuthMethod"
          description: Required when creating a new WAHA session. Optional when linking an existing WAHA session; omitted existing-session links default to qr only if the session still needs pairing.
        waha_session_name:
          type: string
          description: Existing WAHA session name to link. Omit to create a new server-named session.
          example: user-user_35eh25qkkIVwab803vgofO1uSsl-receiving
        phone_number:
          type: string
          example: "31612345678"
        otp_method:
          type: string
          description: Optional WAHA OTP delivery method. Omit or send an empty string for Web pairing; WAHA also accepts sms or voice for registration OTP delivery.
          example: ""
        label:
          type: string
          example: Main receiving session
      additionalProperties: false
    PatchSessionRequest:
      type: object
      required:
        - label
      properties:
        label:
          type: string
          minLength: 1
          example: Trade desk inbox
      additionalProperties: false
    SessionQrResponse:
      type: object
      required:
        - value
      properties:
        value:
          type: string
        request_id:
          type: string
    SessionAuthCodeResponse:
      type: object
      required:
        - phone_number
      properties:
        phone_number:
          type: string
        method:
          type: string
        code:
          type: string
        request_id:
          type: string
    RequestSessionAuthCodeRequest:
      type: object
      required:
        - phone_number
      properties:
        phone_number:
          type: string
          example: "31612345678"
        otp_method:
          type: string
          description: Optional WAHA OTP delivery method. Omit or send an empty string for Web pairing; WAHA also accepts sms or voice for registration OTP delivery.
          example: ""
      additionalProperties: false
    CreateSessionResponse:
      type: object
      required:
        - status
        - session_id
        - waha_session_name
        - role
        - engine
        - session_status
        - created
        - started
        - auth_method
        - webhook_configured
        - sync_requested
        - request_id
      properties:
        status:
          type: string
          enum:
            - accepted
            - rejected
        event_ids:
          type: array
          items:
            type: string
            format: uuid
        session_id:
          type: string
          format: uuid
        waha_session_name:
          type: string
        label:
          type: string
        role:
          $ref: "#/components/schemas/SessionRole"
        engine:
          $ref: "#/components/schemas/SessionEngine"
        session_status:
          type: string
        created:
          type: boolean
        started:
          type: boolean
        auth_method:
          $ref: "#/components/schemas/SessionAuthMethod"
        qr:
          $ref: "#/components/schemas/SessionQrResponse"
        otp:
          $ref: "#/components/schemas/SessionAuthCodeResponse"
        webhook_configured:
          type: boolean
        sync_requested:
          type: boolean
        sync_request_event_ids:
          type: array
          items:
            type: string
            format: uuid
        request_id:
          type: string
    GroupSummary:
      type: object
      required:
        - id
        - session_id
        - waha_group_id
        - name
        - enabled
        - last_seen_at_ms
      properties:
        id:
          type: string
          format: uuid
        session_id:
          type: string
          format: uuid
        waha_group_id:
          type: string
          example: 120363000000000000@g.us
        name:
          type: string
          example: Rolex Europe
        participants_count:
          type: integer
          nullable: true
        enabled:
          type: boolean
        last_seen_at_ms:
          type: integer
          format: int64
    GroupDetail:
      allOf:
        - $ref: "#/components/schemas/GroupSummary"
        - type: object
          required:
            - session_waha_name
            - updated_at_ms
          properties:
            session_waha_name:
              type: string
              example: user-user_35eh25qkkIVwab803vgofO1uSsl-receiving
            updated_at_ms:
              type: integer
              format: int64
    GroupPage:
      type: object
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/GroupSummary"
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
    UpdateGroupSelectionRequest:
      type: object
      required:
        - enabled
      properties:
        enabled:
          type: boolean
      additionalProperties: false
    BulkUpdateGroupSelectionRequest:
      type: object
      required:
        - group_ids
        - enabled
      properties:
        group_ids:
          type: array
          minItems: 1
          maxItems: 200
          items:
            type: string
            format: uuid
        enabled:
          type: boolean
      additionalProperties: false
    ViewProjection:
      type: string
      description: Response projection requested by a saved view.
      enum:
        - message_card
        - market_listing_card
        - contact_profile_card
    PhonePrefixSetFilter:
      type: object
      description: Include/exclude filters for E.164 country phone prefixes without a leading plus sign.
      properties:
        include:
          type: array
          items:
            type: string
          example:
            - "31"
            - "852"
        exclude:
          type: array
          items:
            type: string
          example:
            - "86"
      additionalProperties: false
    NumericRangeFilter:
      type: object
      description: Inclusive numeric range filter. At least one bound is normally supplied.
      properties:
        gte:
          type: number
          format: double
        lte:
          type: number
          format: double
      additionalProperties: false
    Int64RangeFilter:
      type: object
      description: Inclusive int64 range filter. Used for Unix millisecond timestamps.
      properties:
        gte:
          type: integer
          format: int64
        lte:
          type: integer
          format: int64
      additionalProperties: false
    MoneyRangeFilter:
      type: object
      description: Inclusive price range in minor units for the supplied currency.
      properties:
        gte:
          type: integer
          format: int64
        lte:
          type: integer
          format: int64
        currency:
          type: string
          minLength: 3
          maxLength: 3
          example: EUR
      additionalProperties: false
    ViewSide:
      type: string
      enum:
        - for_sale
        - want_to_buy
    ViewFilters:
      type: object
      description: Universal saved-view filter grammar. Each application surface applies the filters it understands.
      properties:
        q:
          type: string
          description: Broad free-text search.
        text_search:
          $ref: "#/components/schemas/ViewTextSearchFilter"
        country_codes:
          $ref: "#/components/schemas/PhonePrefixSetFilter"
        phone_numbers_include:
          type: array
          description: Exact sender phone numbers to include, normalized without spaces or a leading plus sign.
          items:
            type: string
          example:
            - "31612345678"
        phone_numbers_exclude:
          type: array
          description: Exact sender phone numbers to exclude, normalized without spaces or a leading plus sign.
          items:
            type: string
          example:
            - "85212345678"
        side:
          type: array
          description: Market listing side.
          items:
            $ref: "#/components/schemas/ViewSide"
          example:
            - for_sale
        brand:
          type: array
          items:
            type: string
          example:
            - Rolex
        model:
          type: array
          items:
            type: string
          example:
            - Submariner
        reference:
          type: array
          items:
            type: string
          example:
            - 126610LN
        price:
          $ref: "#/components/schemas/MoneyRangeFilter"
        price_delta_pct:
          $ref: "#/components/schemas/NumericRangeFilter"
        message_timestamp_ms:
          $ref: "#/components/schemas/Int64RangeFilter"
        last_seen_at_ms:
          $ref: "#/components/schemas/Int64RangeFilter"
        classification_status:
          type: array
          items:
            $ref: "#/components/schemas/ClassificationStatus"
        group_ids:
          type: array
          items:
            type: string
            format: uuid
        contact_ids:
          type: array
          items:
            type: string
            format: uuid
        has_media:
          type: boolean
      additionalProperties: true
      example:
        side:
          - for_sale
        brand:
          - Rolex
        reference:
          - 126610LN
        country_codes:
          include:
            - "31"
        phone_numbers_exclude:
          - "85212345678"
        price:
          lte: 1200000
          currency: EUR
        message_timestamp_ms:
          gte: 1777800000000
    ViewSortField:
      type: string
      enum:
        - message_timestamp_ms
        - price_amount_minor
        - price_eur_minor
        - price_delta_pct
        - last_seen_at_ms
        - created_at_ms
        - updated_at_ms
        - brand
        - reference
        - occurrence_count
        - group_count
    ViewSortDirection:
      type: string
      enum:
        - asc
        - desc
    ViewSortItem:
      type: object
      required:
        - field
        - direction
      properties:
        field:
          $ref: "#/components/schemas/ViewSortField"
        direction:
          $ref: "#/components/schemas/ViewSortDirection"
      additionalProperties: false
    ViewSort:
      type: array
      description: Ordered sort clauses for a saved view.
      items:
        $ref: "#/components/schemas/ViewSortItem"
      example:
        - field: message_timestamp_ms
          direction: desc
    ViewItem:
      type: object
      required:
        - id
        - name
        - filters
        - sort
        - projection
        - is_system
        - created_at_ms
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        tab_key:
          type: string
          nullable: true
        filters:
          $ref: "#/components/schemas/ViewFilters"
        sort:
          $ref: "#/components/schemas/ViewSort"
        projection:
          $ref: "#/components/schemas/ViewProjection"
        display_order:
          type: integer
          nullable: true
        is_system:
          type: boolean
        created_by:
          type: string
          nullable: true
        created_at_ms:
          type: integer
          format: int64
        updated_at_ms:
          type: integer
          format: int64
    ViewPage:
      type: object
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ViewItem"
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
    CreateViewRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        tab_key:
          type: string
          nullable: true
        filters:
          $ref: "#/components/schemas/ViewFilters"
        sort:
          $ref: "#/components/schemas/ViewSort"
        projection:
          $ref: "#/components/schemas/ViewProjection"
        display_order:
          type: integer
          nullable: true
      additionalProperties: false
    PatchViewRequest:
      type: object
      properties:
        name:
          type: string
        tab_key:
          type: string
          nullable: true
        filters:
          $ref: "#/components/schemas/ViewFilters"
        sort:
          $ref: "#/components/schemas/ViewSort"
        projection:
          $ref: "#/components/schemas/ViewProjection"
        display_order:
          type: integer
          nullable: true
      additionalProperties: false
    PreviewViewRequest:
      type: object
      properties:
        cursor:
          type: string
        page_size:
          type: integer
          minimum: 1
          maximum: 200
      additionalProperties: false
    BlockPhoneRequest:
      type: object
      required:
        - phone_number
      properties:
        phone_number:
          type: string
      additionalProperties: false
    BlockedPhonesResponse:
      type: object
      required:
        - blocked_phones
        - total
      properties:
        blocked_phones:
          type: array
          items:
            type: string
        total:
          type: integer
    ClassificationStatus:
      type: string
      enum:
        - pending
        - completed
        - skipped
        - failed
    AIConnectorId:
      type: string
      enum:
        - openai
        - anthropic
        - gemini
        - openrouter
        - openai_compatible
    AISchemaMode:
      type: string
      enum:
        - json_schema
        - tool_schema
        - json_object
    ModelProfile:
      type: object
      required:
        - id
        - connector_id
        - model_id
        - schema_mode
        - display_name
        - default
        - enabled
      properties:
        id:
          type: string
          example: openrouter:anthropic/claude-sonnet-4.5
        connector_id:
          $ref: "#/components/schemas/AIConnectorId"
        model_id:
          type: string
          example: anthropic/claude-sonnet-4.5
        schema_mode:
          $ref: "#/components/schemas/AISchemaMode"
        display_name:
          type: string
        description:
          type: string
        default:
          type: boolean
        enabled:
          type: boolean
      additionalProperties: false
    AIConnectorStatus:
      type: object
      required:
        - connector_id
        - configured
      properties:
        connector_id:
          $ref: "#/components/schemas/AIConnectorId"
        configured:
          type: boolean
        key_fingerprint:
          type: string
        updated_at_ms:
          type: integer
          format: int64
      additionalProperties: false
    ClassificationSettings:
      type: object
      required:
        - connector_id
        - model_profile_id
        - model_id
        - schema_mode
        - prompt_bundle_version
      properties:
        connector_id:
          $ref: "#/components/schemas/AIConnectorId"
        model_profile_id:
          type: string
        model_id:
          type: string
        schema_mode:
          $ref: "#/components/schemas/AISchemaMode"
        prompt_bundle_version:
          type: string
        updated_at_ms:
          type: integer
          format: int64
      additionalProperties: false
    ModelProfilesResponse:
      type: object
      required:
        - profiles
        - connectors
        - current_settings
      properties:
        profiles:
          type: array
          items:
            $ref: "#/components/schemas/ModelProfile"
        connectors:
          type: array
          items:
            $ref: "#/components/schemas/AIConnectorStatus"
        current_settings:
          $ref: "#/components/schemas/ClassificationSettings"
      additionalProperties: false
    ConnectorKeyRequest:
      type: object
      required:
        - api_key
      properties:
        api_key:
          type: string
          minLength: 1
      additionalProperties: false
    UpdateClassificationSettingsRequest:
      type: object
      required:
        - model_profile_id
      properties:
        model_profile_id:
          type: string
      additionalProperties: false
    ActionCategory:
      type: string
      enum:
        - view
        - alert
        - group
        - workspace
        - ai_settings
        - outbound
        - system
    ActionRevertStatus:
      type: string
      enum:
        - revertible
        - irreversible
        - reverted
    ActionPreviewStatus:
      type: string
      enum:
        - ready
        - conflict
        - irreversible
        - already_reverted
    ActionValueType:
      type: string
      enum:
        - string
        - number
        - boolean
        - timestamp_ms
        - enum
        - list
        - redacted
    ActionEntityRef:
      type: object
      required:
        - type
        - id
      properties:
        type:
          type: string
          example: saved_view
        id:
          type: string
          example: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7070
        label:
          type: string
          nullable: true
          example: Rolex feed
      additionalProperties: false
    ActionChange:
      type: object
      required:
        - field
        - label
        - value_type
      properties:
        field:
          type: string
          example: name
        label:
          type: string
          example: Name
        before:
          type: string
          nullable: true
        after:
          type: string
          nullable: true
        value_type:
          $ref: "#/components/schemas/ActionValueType"
      additionalProperties: false
    ActionSource:
      type: object
      required:
        - kind
      properties:
        kind:
          type: string
          enum:
            - api
        request_id:
          type: string
          nullable: true
        service:
          type: string
          nullable: true
      additionalProperties: false
    ActionItem:
      type: object
      required:
        - id
        - actor_user_id
        - correlation_id
        - event_ids
        - category
        - action
        - summary
        - occurred_at_ms
        - entity
        - related_entities
        - changes
        - revert_status
        - source
      properties:
        id:
          type: string
          format: uuid
        actor_user_id:
          type: string
        correlation_id:
          type: string
          format: uuid
        event_ids:
          type: array
          items:
            type: string
            format: uuid
        category:
          $ref: "#/components/schemas/ActionCategory"
        action:
          type: string
          example: saved_view_created
        summary:
          type: string
          example: Saved view created
        occurred_at_ms:
          type: integer
          format: int64
        entity:
          $ref: "#/components/schemas/ActionEntityRef"
        related_entities:
          type: array
          items:
            $ref: "#/components/schemas/ActionEntityRef"
        changes:
          type: array
          items:
            $ref: "#/components/schemas/ActionChange"
        revert_status:
          $ref: "#/components/schemas/ActionRevertStatus"
        reverted_by_action_id:
          type: string
          format: uuid
          nullable: true
        source:
          $ref: "#/components/schemas/ActionSource"
      additionalProperties: false
    ActionPage:
      type: object
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ActionItem"
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
      additionalProperties: false
    ActionExpectedVersion:
      type: object
      required:
        - stream_type
        - stream_id
        - latest_sequence
      properties:
        stream_type:
          type: string
          example: saved_view
        stream_id:
          type: string
          example: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7070
        latest_sequence:
          type: integer
          format: int64
      additionalProperties: false
    ActionRevertEffect:
      type: object
      required:
        - entity
        - summary
      properties:
        entity:
          $ref: "#/components/schemas/ActionEntityRef"
        summary:
          type: string
          example: Delete the saved view created by this action.
      additionalProperties: false
    ActionRevertConflict:
      type: object
      required:
        - entity
        - latest_event_id
        - latest_event_type
        - latest_occurred_at_ms
      properties:
        entity:
          $ref: "#/components/schemas/ActionEntityRef"
        latest_action_id:
          type: string
          format: uuid
          nullable: true
        latest_event_id:
          type: string
          format: uuid
        latest_event_type:
          type: string
          example: SavedViewUpdated
        latest_occurred_at_ms:
          type: integer
          format: int64
      additionalProperties: false
    ActionRevertPreview:
      type: object
      required:
        - action_id
        - status
        - effects
        - conflicts
        - expected_versions
      properties:
        action_id:
          type: string
          format: uuid
        status:
          $ref: "#/components/schemas/ActionPreviewStatus"
        message:
          type: string
          nullable: true
        effects:
          type: array
          items:
            $ref: "#/components/schemas/ActionRevertEffect"
        conflicts:
          type: array
          items:
            $ref: "#/components/schemas/ActionRevertConflict"
        expected_versions:
          type: array
          items:
            $ref: "#/components/schemas/ActionExpectedVersion"
      additionalProperties: false
    ActionRevertRequest:
      type: object
      required:
        - idempotency_key
        - expected_versions
      properties:
        idempotency_key:
          type: string
          minLength: 1
          example: frontend-action-revert:018f06e2-4f8d-7c01-8a0d-cc6fd78f7070:1780509605123:018f06e2-4f8d-7c01-8a0d-cc6fd78f7071
        expected_versions:
          type: array
          items:
            $ref: "#/components/schemas/ActionExpectedVersion"
      additionalProperties: false
    EntityLogEntityType:
      type: string
      enum:
        - watch_listing
        - classified_watch
        - classification
        - canonical_message
        - message_occurrence
        - classification_settings
        - ai_connector
        - saved_view
        - whatsapp_session
        - whatsapp_group
        - market_alert
        - contact_profile
        - outbound_text_send
        - workspace
    EntityLogScope:
      type: string
      description: "`timeline` includes curated related events; `direct` includes only events where the requested entity is primary."
      default: timeline
      enum:
        - timeline
        - direct
    EntityLogCategory:
      type: string
      enum:
        - ingest
        - session
        - group
        - message
        - media
        - classification
        - market
        - ai_settings
        - view
        - alert
        - profile
        - replay
        - system
    EntityLogActorType:
      type: string
      enum:
        - user
        - admin
        - system
        - waha
        - temporal
        - replay
        - projector
    EntityLogSourceKind:
      type: string
      enum:
        - api
        - webhook
        - projector
        - worker
        - replay
        - migration
        - system
    EntityLogValueType:
      type: string
      enum:
        - string
        - number
        - boolean
        - money
        - timestamp_ms
        - enum
        - list
        - redacted
    EntityLogEntityRef:
      type: object
      required:
        - type
        - id
      properties:
        type:
          $ref: "#/components/schemas/EntityLogEntityType"
        id:
          type: string
        label:
          type: string
          nullable: true
      additionalProperties: false
    EntityLogActor:
      type: object
      required:
        - type
      properties:
        type:
          $ref: "#/components/schemas/EntityLogActorType"
        id:
          type: string
          nullable: true
        display_name:
          type: string
          nullable: true
      additionalProperties: false
    EntityLogSource:
      type: object
      required:
        - kind
      properties:
        kind:
          $ref: "#/components/schemas/EntityLogSourceKind"
        request_id:
          type: string
          nullable: true
        command_id:
          type: string
          nullable: true
        service:
          type: string
          nullable: true
      additionalProperties: false
    EntityLogChange:
      type: object
      required:
        - field
        - label
        - value_type
      properties:
        field:
          type: string
          example: model_profile_id
        label:
          type: string
          example: Model profile
        before:
          type: string
          nullable: true
        after:
          type: string
          nullable: true
        value_type:
          $ref: "#/components/schemas/EntityLogValueType"
      additionalProperties: false
    EntityLogDetail:
      type: object
      required:
        - key
        - value
      properties:
        key:
          type: string
          example: connector_id
        label:
          type: string
          nullable: true
          example: Connector
        value:
          type: string
          nullable: true
          example: openrouter
      additionalProperties: false
    EntityLogItem:
      type: object
      required:
        - id
        - event_id
        - event_type
        - event_version
        - category
        - action
        - summary
        - occurred_at_ms
        - actor
        - entity
        - related_entities
        - changes
        - details
        - source
        - redacted
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: string
          format: uuid
        event_type:
          type: string
          example: ClassificationModelSelected
        event_version:
          type: integer
          example: 1
        category:
          $ref: "#/components/schemas/EntityLogCategory"
        action:
          type: string
          example: classification_model_selected
        summary:
          type: string
          example: Classification model changed
        occurred_at_ms:
          type: integer
          format: int64
        actor:
          $ref: "#/components/schemas/EntityLogActor"
        entity:
          $ref: "#/components/schemas/EntityLogEntityRef"
        related_entities:
          type: array
          items:
            $ref: "#/components/schemas/EntityLogEntityRef"
        changes:
          type: array
          items:
            $ref: "#/components/schemas/EntityLogChange"
        details:
          type: array
          items:
            $ref: "#/components/schemas/EntityLogDetail"
        source:
          $ref: "#/components/schemas/EntityLogSource"
        redacted:
          type: boolean
          description: True when sensitive source values were intentionally omitted or replaced.
      additionalProperties: false
    EntityLogPage:
      type: object
      required:
        - entity
        - scope
        - items
        - has_more
      properties:
        entity:
          $ref: "#/components/schemas/EntityLogEntityRef"
        scope:
          $ref: "#/components/schemas/EntityLogScope"
        items:
          type: array
          items:
            $ref: "#/components/schemas/EntityLogItem"
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
      additionalProperties: false
    WatchSetComponent:
      type: string
      enum:
        - WATCH
        - BOX
        - PAPERS
        - CARD
    ClassifiedWatch:
      type: object
      required:
        - brand
        - model
        - reference
        - year
        - price_amount
        - currency_code
        - expected_price_usd
        - condition
        - full_set_status
        - variant
        - dial_type
      properties:
        brand:
          type: string
          nullable: true
        model:
          type: string
          nullable: true
        reference:
          type: string
          nullable: true
        year:
          type: string
          nullable: true
        price_amount:
          type: integer
          nullable: true
        currency_code:
          type: string
          nullable: true
        expected_price_usd:
          type: integer
          nullable: true
        condition:
          type: string
          nullable: true
          enum:
            - NEW
            - LIKE_NEW
            - USED
        full_set_status:
          type: array
          nullable: true
          items:
            $ref: "#/components/schemas/WatchSetComponent"
        variant:
          type: string
          nullable: true
        dial_type:
          type: string
          nullable: true
      additionalProperties: false
    LegacyClassificationOutput:
      type: object
      required:
        - message_type
        - watches
      properties:
        message_type:
          type: string
          enum:
            - buying
            - selling
            - personal
        watches:
          type: array
          items:
            $ref: "#/components/schemas/ClassifiedWatch"
      additionalProperties: false
    ClassificationItem:
      type: object
      required:
        - id
        - canonical_message_id
        - classification_job_id
        - message_type
        - watches
        - legacy_output
        - normalized_output
        - completed_at_ms
        - created_at_ms
      properties:
        id:
          type: string
          format: uuid
        canonical_message_id:
          type: string
          format: uuid
        classification_job_id:
          type: string
          format: uuid
        message_type:
          type: string
          enum:
            - buying
            - selling
            - personal
        watches:
          type: array
          items:
            $ref: "#/components/schemas/ClassifiedWatch"
        legacy_output:
          $ref: "#/components/schemas/LegacyClassificationOutput"
        normalized_output:
          $ref: "#/components/schemas/LegacyClassificationOutput"
        extraction_output:
          nullable: true
          type: object
          description: Rich watch extraction envelope with document context, source spans, inherited context, field evidence and validation warnings.
        raw_model_response:
          nullable: true
          type: object
          description: Raw structured provider response when it is a JSON object.
        connector_id:
          $ref: "#/components/schemas/AIConnectorId"
        model_profile_id:
          type: string
        model_id:
          type: string
        schema_mode:
          $ref: "#/components/schemas/AISchemaMode"
        prompt_bundle_version:
          type: string
        latency_ms:
          type: integer
        input_tokens:
          type: integer
        output_tokens:
          type: integer
        total_tokens:
          type: integer
        completed_at_ms:
          type: integer
          format: int64
        created_at_ms:
          type: integer
          format: int64
      additionalProperties: false
    ClassificationPage:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ClassificationItem"
        next_cursor:
          type: string
          nullable: true
      additionalProperties: false
    CorrectClassificationRequest:
      type: object
      required:
        - output
      properties:
        output:
          $ref: "#/components/schemas/LegacyClassificationOutput"
      additionalProperties: false
    MessageIntent:
      type: string
      enum:
        - selling
        - buying
        - personal
        - unknown
    MessageSearchMode:
      type: string
      enum:
        - broad
        - exact
        - exact_light
      description: broad matches any normalized word; exact matches the normalized phrase; exact_light matches ordered words within a small positional gap.
    ViewTextSearchFilter:
      type: object
      required:
        - query
      properties:
        query:
          type: string
          minLength: 1
        mode:
          $ref: "#/components/schemas/MessageSearchMode"
      additionalProperties: false
    SenderSummary:
      type: object
      required:
        - contact_id
      properties:
        contact_id:
          type: string
          format: uuid
        phone_number:
          type: string
          nullable: true
        lid:
          type: string
          nullable: true
        display_name:
          type: string
          nullable: true
        country_prefix:
          type: string
          nullable: true
    MediaAssetSummary:
      type: object
      required:
        - id
        - status
        - created_at_ms
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        original_url:
          type: string
          nullable: true
        public_url:
          type: string
          nullable: true
        storage_bucket:
          type: string
          nullable: true
        storage_key:
          type: string
          nullable: true
        mime_type:
          type: string
          nullable: true
        filename:
          type: string
          nullable: true
        size_bytes:
          type: integer
          format: int64
          nullable: true
        status:
          type: string
          example: pending_storage
        created_at_ms:
          type: integer
          format: int64
        updated_at_ms:
          type: integer
          format: int64
    MessageSummary:
      type: object
      required:
        - id
        - primary_occurrence_id
        - message_timestamp_ms
        - first_seen_at_ms
        - last_seen_at_ms
        - sender
        - group_names
        - group_ids
        - group_count
        - occurrence_count
        - has_media
        - classification_status
        - message_intent
        - is_edited
        - is_revoked
      properties:
        id:
          type: string
          format: uuid
        primary_occurrence_id:
          type: string
          format: uuid
        message_text:
          type: string
          nullable: true
        message_timestamp_ms:
          type: integer
          format: int64
        first_seen_at_ms:
          type: integer
          format: int64
        last_seen_at_ms:
          type: integer
          format: int64
        sender:
          $ref: "#/components/schemas/SenderSummary"
        group_names:
          type: array
          items:
            type: string
        group_ids:
          type: array
          items:
            type: string
            format: uuid
        group_count:
          type: integer
        occurrence_count:
          type: integer
        has_media:
          type: boolean
        primary_media_asset_id:
          type: string
          format: uuid
          nullable: true
        primary_media_url:
          type: string
          nullable: true
        classification_status:
          $ref: "#/components/schemas/ClassificationStatus"
        message_intent:
          $ref: "#/components/schemas/MessageIntent"
        is_edited:
          type: boolean
        is_revoked:
          type: boolean
    MessagePage:
      type: object
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/MessageSummary"
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
    MessageOccurrence:
      type: object
      required:
        - id
        - canonical_message_id
        - waha_message_id
        - session_id
        - waha_session_name
        - session_label
        - group_id
        - waha_group_id
        - group_name
        - sender
        - sender_raw_id
        - message_timestamp_ms
        - has_media
        - media_asset_ids
        - is_edited
        - is_revoked
        - source_event_id
        - created_at_ms
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        canonical_message_id:
          type: string
          format: uuid
        waha_message_id:
          type: string
        session_id:
          type: string
          format: uuid
        waha_session_name:
          type: string
        session_label:
          type: string
        group_id:
          type: string
          format: uuid
        waha_group_id:
          type: string
        group_name:
          type: string
        sender:
          $ref: "#/components/schemas/SenderSummary"
        sender_raw_id:
          type: string
        message_text:
          type: string
          nullable: true
        message_timestamp_ms:
          type: integer
          format: int64
        has_media:
          type: boolean
        media_asset_ids:
          type: array
          items:
            type: string
            format: uuid
        is_edited:
          type: boolean
        is_revoked:
          type: boolean
        source_event_id:
          type: string
          format: uuid
        created_at_ms:
          type: integer
          format: int64
        updated_at_ms:
          type: integer
          format: int64
    OccurrencePage:
      type: object
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/MessageOccurrence"
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
    OccurrenceSummaryGroup:
      type: object
      required:
        - group_id
        - group_name
        - occurrence_count
        - first_seen_at_ms
        - last_seen_at_ms
        - session_labels
        - edited_count
        - revoked_count
        - media_count
      properties:
        group_id:
          type: string
          format: uuid
        group_name:
          type: string
        occurrence_count:
          type: integer
        first_seen_at_ms:
          type: integer
          format: int64
        last_seen_at_ms:
          type: integer
          format: int64
        session_labels:
          type: array
          items:
            type: string
        edited_count:
          type: integer
        revoked_count:
          type: integer
        media_count:
          type: integer
    OccurrenceSummaryDay:
      type: object
      required:
        - day_start_ms
        - occurrence_count
        - group_count
        - latest_at_ms
        - groups
      properties:
        day_start_ms:
          type: integer
          format: int64
        occurrence_count:
          type: integer
        group_count:
          type: integer
        latest_at_ms:
          type: integer
          format: int64
        groups:
          type: array
          items:
            $ref: "#/components/schemas/OccurrenceSummaryGroup"
    OccurrenceSummary:
      type: object
      required:
        - total_count
        - group_count
        - session_count
        - days
      properties:
        total_count:
          type: integer
        group_count:
          type: integer
        session_count:
          type: integer
        first_seen_at_ms:
          type: integer
          format: int64
          nullable: true
        last_seen_at_ms:
          type: integer
          format: int64
          nullable: true
        days:
          type: array
          items:
            $ref: "#/components/schemas/OccurrenceSummaryDay"
    MessageDetail:
      allOf:
        - $ref: "#/components/schemas/MessageSummary"
        - type: object
          required:
            - occurrences
            - occurrences_has_more
            - media
          properties:
            occurrences:
              type: array
              items:
                $ref: "#/components/schemas/MessageOccurrence"
            occurrences_next_cursor:
              type: string
              nullable: true
            occurrences_has_more:
              type: boolean
            media:
              type: array
              items:
                $ref: "#/components/schemas/MediaAssetSummary"
    MarketSide:
      type: string
      enum:
        - for_sale
        - want_to_buy
    MarketMessageTypeFilter:
      type: string
      enum:
        - selling
        - buying
        - for_sale
        - want_to_buy
    MarketSortField:
      type: string
      enum:
        - message_timestamp_ms
        - last_seen_at_ms
        - price_eur_minor
        - created_at_ms
        - updated_at_ms
        - brand
        - reference
    MarketSearchInclude:
      type: string
      enum:
        - summary
        - facets
    MarketSortItem:
      type: object
      required:
        - field
        - direction
      properties:
        field:
          $ref: "#/components/schemas/MarketSortField"
        direction:
          $ref: "#/components/schemas/ViewSortDirection"
      additionalProperties: false
    MarketYearFilter:
      type: object
      properties:
        values:
          type: array
          items:
            type: integer
          example:
            - 2022
            - 2023
        gte:
          type: integer
          example: 2018
        lte:
          type: integer
          example: 2026
      additionalProperties: false
    MarketFullSetFilterValue:
      type: string
      enum:
        - WATCH
        - BOX
        - PAPERS
        - CARD
        - FULL_SET
        - WATCH_ONLY
    MarketPhoneNumberSetFilter:
      type: object
      properties:
        include:
          type: array
          items:
            type: string
          example:
            - "31612345678"
        exclude:
          type: array
          items:
            type: string
          example:
            - "85212345678"
      additionalProperties: false
    MarketFilters:
      type: object
      description: Flexible market filter grammar. Documented fields are typed; additional fields are tolerated for forward-compatible frontend experiments and ignored until implemented.
      properties:
        q:
          type: string
          description: Broad text search over listing, message, sender and group search text.
          example: Rolex 126610LN
        text_search:
          $ref: "#/components/schemas/ViewTextSearchFilter"
        side:
          type: array
          items:
            $ref: "#/components/schemas/MarketSide"
          example:
            - for_sale
        message_type:
          type: array
          description: Legacy aliases accepted by the API and normalized to market side.
          items:
            $ref: "#/components/schemas/MarketMessageTypeFilter"
        brand:
          type: array
          items:
            type: string
          example:
            - Rolex
        model:
          type: array
          items:
            type: string
          example:
            - Submariner
        reference:
          type: array
          items:
            type: string
          example:
            - 126610LN
        year:
          $ref: "#/components/schemas/MarketYearFilter"
        years:
          type: array
          items:
            type: integer
          example:
            - 2023
        price:
          $ref: "#/components/schemas/MoneyRangeFilter"
        condition:
          type: array
          items:
            type: string
            enum:
              - NEW
              - LIKE_NEW
              - USED
          example:
            - NEW
        conditions:
          type: array
          items:
            type: string
            enum:
              - NEW
              - LIKE_NEW
              - USED
        full_set_status:
          type: array
          items:
            $ref: "#/components/schemas/MarketFullSetFilterValue"
        full_set_options:
          type: array
          items:
            $ref: "#/components/schemas/MarketFullSetFilterValue"
        dial_type:
          type: array
          items:
            type: string
          example:
            - Wimbledon
        dial_types:
          type: array
          items:
            type: string
        variant:
          type: array
          items:
            type: string
        variants:
          type: array
          items:
            type: string
        country_codes:
          $ref: "#/components/schemas/PhonePrefixSetFilter"
        phone_numbers_include:
          type: array
          items:
            type: string
        phone_numbers_exclude:
          type: array
          items:
            type: string
        phone_numbers:
          $ref: "#/components/schemas/MarketPhoneNumberSetFilter"
        group_ids:
          type: array
          items:
            type: string
            format: uuid
        group_names:
          type: array
          items:
            type: string
        contact_ids:
          type: array
          items:
            type: string
            format: uuid
        contact_id:
          type: string
          format: uuid
        phone_number:
          type: string
        canonical_message_id:
          type: string
          format: uuid
          description: Restrict results to listings extracted from a single canonical message.
        canonical_message_ids:
          type: array
          items:
            type: string
            format: uuid
        has_media:
          type: boolean
        message_timestamp_ms:
          $ref: "#/components/schemas/Int64RangeFilter"
        last_seen_at_ms:
          $ref: "#/components/schemas/Int64RangeFilter"
        from_ms:
          type: integer
          format: int64
        to_ms:
          type: integer
          format: int64
        include_duplicates:
          type: boolean
          default: false
        include_hidden:
          type: boolean
          default: false
        include_revoked:
          type: boolean
          default: false
        exclude_outliers:
          type: boolean
          default: false
      additionalProperties: true
    MarketSearchRequest:
      type: object
      properties:
        view_id:
          type: string
          format: uuid
          description: Saved view filters are combined with request filters using AND semantics.
        filters:
          $ref: "#/components/schemas/MarketFilters"
        sort:
          type: array
          description: The current implementation applies the first sort item and falls back to newest listing activity.
          items:
            $ref: "#/components/schemas/MarketSortItem"
        page_size:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
        cursor:
          type: string
        display_currency:
          type: string
          minLength: 3
          maxLength: 3
          example: USD
        include:
          type: array
          items:
            $ref: "#/components/schemas/MarketSearchInclude"
      additionalProperties: false
      example:
        view_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7001
        filters:
          side:
            - for_sale
          reference:
            - 126610LN
          price:
            lte: 1200000
            currency: EUR
          country_codes:
            include:
              - "31"
        sort:
          - field: price_eur_minor
            direction: asc
        page_size: 50
        display_currency: EUR
        include:
          - summary
          - facets
    LiveDealFeedRequest:
      type: object
      properties:
        view_id:
          type: string
          format: uuid
          description: Saved view filters are combined with request filters using AND semantics. Use this for market scopes such as EU.
        filters:
          $ref: "#/components/schemas/MarketFilters"
        min_discount_percent:
          type: number
          format: double
          minimum: 0
          maximum: 100
          default: 5
          description: Minimum percent below the median comp price required for a listing to appear.
        sort:
          type: string
          enum:
            - best_discount
            - newest
          default: best_discount
        page_size:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
        display_currency:
          type: string
          minLength: 3
          maxLength: 3
          example: EUR
      additionalProperties: false
      example:
        view_id: 018f06e2-4f8d-7c01-8a0d-cc6fd78f7001
        filters:
          reference:
            - "126334"
          dial_type:
            - Wimbledon
          variant:
            - Jubilee
          year:
            gte: 2026
            lte: 2026
          condition:
            - NEW
        min_discount_percent: 5
        sort: best_discount
        page_size: 50
        display_currency: EUR
    LiveDealFeedResponse:
      type: object
      required:
        - items
        - comparable_count
        - matching_count
        - min_discount_percent
        - updated_at_ms
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/MarketListingItem"
        comparable_count:
          type: integer
          description: Number of for-sale comp listings in the effective filter set.
        median_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        matching_count:
          type: integer
          description: Total matching cheap listings before page size truncation.
        min_discount_percent:
          type: number
          format: double
        updated_at_ms:
          type: integer
          format: int64
        effective_filter:
          $ref: "#/components/schemas/MarketEffectiveFilter"
      additionalProperties: false
    OfficialVsMarketReferenceRequest:
      type: object
      required:
        - reference
      properties:
        reference:
          type: string
          minLength: 1
          example: 126610LN
        row_id:
          type: string
          nullable: true
          description: Stable client row id echoed in the response so duplicate references can be tracked.
        configuration_key:
          type: string
          nullable: true
          description: Optional official Rolex configuration key for this row.
        market_view_id:
          type: string
          format: uuid
          nullable: true
          description: Optional saved view override for this reference's market average.
        mapping_id:
          type: string
          format: uuid
          nullable: true
          description: Optional saved calculator mapping to apply before row-level overrides.
        filters:
          $ref: "#/components/schemas/MarketFilters"
      additionalProperties: false
    OfficialVsMarketRequest:
      type: object
      properties:
        references:
          type: array
          maxItems: 200
          description: Optional official references to return. If omitted, the endpoint returns the first official rows for the brand.
          items:
            $ref: "#/components/schemas/OfficialVsMarketReferenceRequest"
        view_id:
          type: string
          format: uuid
          nullable: true
          description: Saved view filters are combined with calculator filters and per-reference brand/reference filters.
        filters:
          $ref: "#/components/schemas/MarketFilters"
        sort:
          type: string
          enum:
            - profit_desc
            - profit_asc
            - uplift_desc
            - uplift_asc
            - profit-desc
            - profit-asc
            - uplift-desc
            - uplift-asc
          default: profit_desc
        display_currency:
          type: string
          minLength: 3
          maxLength: 3
          example: EUR
        exclude_outliers:
          type: boolean
          default: true
        page_size:
          type: integer
          minimum: 1
          maximum: 200
          default: 200
      additionalProperties: false
    OfficialReferencePrice:
      type: object
      required:
        - reference
        - reference_normalized
        - status
        - configuration_count
      properties:
        collection:
          type: string
          nullable: true
        model:
          type: string
          nullable: true
        reference:
          type: string
        reference_normalized:
          type: string
        market_reference:
          type: string
          nullable: true
        official_url:
          type: string
          nullable: true
        official_image_url:
          type: string
          nullable: true
        price:
          $ref: "#/components/schemas/MarketMoney"
        locale:
          type: string
          nullable: true
        status:
          type: string
          example: available
        scraped_at_ms:
          type: integer
          format: int64
          nullable: true
        configuration_key:
          type: string
          nullable: true
        configuration_label:
          type: string
          nullable: true
        dial:
          type: string
          nullable: true
        bezel:
          type: string
          nullable: true
        bracelet:
          type: string
          nullable: true
        configuration_count:
          type: integer
      additionalProperties: false
    OfficialVsMarketItem:
      type: object
      required:
        - brand
        - official
        - market_listing_count
        - mapping_status
      properties:
        brand:
          type: string
        row_id:
          type: string
          nullable: true
        official:
          $ref: "#/components/schemas/OfficialReferencePrice"
        market_average:
          $ref: "#/components/schemas/MarketMoney"
        market_listing_count:
          type: integer
        market_profit_amount:
          $ref: "#/components/schemas/MarketMoney"
        market_profit_percent:
          type: number
          format: double
          nullable: true
        sample_media_asset_id:
          type: string
          format: uuid
          nullable: true
        sample_media_url:
          type: string
          nullable: true
        applied_mapping_id:
          type: string
          format: uuid
          nullable: true
        mapping_status:
          type: string
          enum:
            - none
            - default
            - row_override
        effective_filter:
          $ref: "#/components/schemas/MarketEffectiveFilter"
      additionalProperties: false
    OfficialVsMarketResponse:
      type: object
      required:
        - brand
        - total
        - items
      properties:
        brand:
          type: string
        total:
          type: integer
        items:
          type: array
          items:
            $ref: "#/components/schemas/OfficialVsMarketItem"
      additionalProperties: false
    OfficialConfigurationOption:
      type: object
      required:
        - brand
        - reference
        - reference_normalized
        - status
        - is_default
      properties:
        brand:
          type: string
        collection:
          type: string
          nullable: true
        model:
          type: string
          nullable: true
        reference:
          type: string
        reference_normalized:
          type: string
        market_reference:
          type: string
          nullable: true
        official_url:
          type: string
          nullable: true
        official_image_url:
          type: string
          nullable: true
        price:
          $ref: "#/components/schemas/MarketMoney"
        locale:
          type: string
          nullable: true
        status:
          type: string
        scraped_at_ms:
          type: integer
          format: int64
          nullable: true
        configuration_key:
          type: string
          nullable: true
        configuration_label:
          type: string
          nullable: true
        dial:
          type: string
          nullable: true
        bezel:
          type: string
          nullable: true
        bracelet:
          type: string
          nullable: true
        is_default:
          type: boolean
        default_mapping_id:
          type: string
          format: uuid
          nullable: true
        default_market_view_id:
          type: string
          format: uuid
          nullable: true
        default_filters:
          $ref: "#/components/schemas/MarketFilters"
      additionalProperties: false
    OfficialConfigurationOptionsResponse:
      type: object
      required:
        - brand
        - total
        - items
      properties:
        brand:
          type: string
        total:
          type: integer
        items:
          type: array
          items:
            $ref: "#/components/schemas/OfficialConfigurationOption"
      additionalProperties: false
    CalculatorFilterMapping:
      type: object
      required:
        - id
        - tenant_user_id
        - actor_user_id
        - brand
        - reference
        - reference_normalized
        - filters
        - created_at_ms
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        tenant_user_id:
          type: string
        actor_user_id:
          type: string
        brand:
          type: string
        reference:
          type: string
        reference_normalized:
          type: string
        configuration_key:
          type: string
          nullable: true
        configuration_label:
          type: string
          nullable: true
        name:
          type: string
          nullable: true
        market_view_id:
          type: string
          format: uuid
          nullable: true
        filters:
          $ref: "#/components/schemas/MarketFilters"
        created_at_ms:
          type: integer
          format: int64
        updated_at_ms:
          type: integer
          format: int64
      additionalProperties: false
    CalculatorFilterMappingPage:
      type: object
      required:
        - items
        - total
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/CalculatorFilterMapping"
        total:
          type: integer
      additionalProperties: false
    UpsertCalculatorFilterMappingRequest:
      type: object
      required:
        - brand
        - reference
      properties:
        id:
          type: string
          format: uuid
          nullable: true
        brand:
          type: string
        reference:
          type: string
        configuration_key:
          type: string
          nullable: true
        configuration_label:
          type: string
          nullable: true
        name:
          type: string
          nullable: true
        market_view_id:
          type: string
          format: uuid
          nullable: true
        filters:
          $ref: "#/components/schemas/MarketFilters"
      additionalProperties: false
    CalculatorPresetRow:
      type: object
      required:
        - row_id
        - reference
      properties:
        row_id:
          type: string
        reference:
          type: string
        configuration_key:
          type: string
          nullable: true
        market_view_id:
          type: string
          format: uuid
          nullable: true
        mapping_id:
          type: string
          format: uuid
          nullable: true
        filters:
          $ref: "#/components/schemas/MarketFilters"
        sale_price:
          $ref: "#/components/schemas/MarketMoney"
      additionalProperties: false
    CalculatorPreset:
      type: object
      required:
        - id
        - tenant_user_id
        - actor_user_id
        - brand
        - name
        - rows
        - sort
        - created_at_ms
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        tenant_user_id:
          type: string
        actor_user_id:
          type: string
        brand:
          type: string
        name:
          type: string
        rows:
          type: array
          items:
            $ref: "#/components/schemas/CalculatorPresetRow"
        view_id:
          type: string
          format: uuid
          nullable: true
        filters:
          $ref: "#/components/schemas/MarketFilters"
        sort:
          type: string
        created_at_ms:
          type: integer
          format: int64
        updated_at_ms:
          type: integer
          format: int64
      additionalProperties: false
    CalculatorPresetPage:
      type: object
      required:
        - items
        - total
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/CalculatorPreset"
        total:
          type: integer
      additionalProperties: false
    CreateCalculatorPresetRequest:
      type: object
      required:
        - brand
        - name
        - rows
      properties:
        brand:
          type: string
        name:
          type: string
        rows:
          type: array
          maxItems: 200
          items:
            $ref: "#/components/schemas/CalculatorPresetRow"
        view_id:
          type: string
          format: uuid
          nullable: true
        filters:
          $ref: "#/components/schemas/MarketFilters"
        sort:
          type: string
      additionalProperties: false
    PatchCalculatorPresetRequest:
      type: object
      properties:
        name:
          type: string
        rows:
          type: array
          maxItems: 200
          items:
            $ref: "#/components/schemas/CalculatorPresetRow"
        view_id:
          type: string
          format: uuid
          nullable: true
        filters:
          $ref: "#/components/schemas/MarketFilters"
        sort:
          type: string
      additionalProperties: false
    MarketMoney:
      type: object
      required:
        - amount_minor
        - currency
      properties:
        amount_minor:
          type: integer
          format: int64
        currency:
          type: string
          minLength: 3
          maxLength: 3
      additionalProperties: false
    MarketListingSource:
      type: object
      required:
        - canonical_message_id
        - message_timestamp_ms
        - group_ids
        - group_names
        - occurrence_count
        - first_seen_at_ms
        - last_seen_at_ms
        - has_media
        - is_duplicate
        - is_hidden
        - is_revoked
      properties:
        canonical_message_id:
          type: string
          format: uuid
        message_text:
          type: string
          nullable: true
        message_timestamp_ms:
          type: integer
          format: int64
        group_ids:
          type: array
          items:
            type: string
            format: uuid
        group_names:
          type: array
          items:
            type: string
        occurrence_count:
          type: integer
        first_seen_at_ms:
          type: integer
          format: int64
        last_seen_at_ms:
          type: integer
          format: int64
        has_media:
          type: boolean
        media_asset_id:
          type: string
          format: uuid
          nullable: true
        media_url:
          type: string
          nullable: true
          description: Preferred public URL for the media attached to the canonical message, when known. Resolves to the media asset's public_url (S3-compatible storage) with original_url as a fallback.
        is_duplicate:
          type: boolean
        is_hidden:
          type: boolean
        is_revoked:
          type: boolean
      additionalProperties: false
    MarketPriceIndicator:
      type: object
      required:
        - comparable_count
      properties:
        median_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        delta_eur_minor:
          type: integer
          format: int64
          nullable: true
        delta_percent:
          type: number
          format: double
          nullable: true
        comparable_count:
          type: integer
      additionalProperties: false
    MarketListingFieldEvidence:
      type: object
      properties:
        value:
          nullable: true
        source_type:
          type: string
          nullable: true
        source_text:
          type: string
          nullable: true
        source_line_start:
          type: integer
          nullable: true
        source_line_end:
          type: integer
          nullable: true
        rule:
          type: string
          nullable: true
        confidence:
          type: number
          format: double
          nullable: true
      additionalProperties: false
    MarketListingGlobalContext:
      type: object
      properties:
        intent:
          type: string
          nullable: true
        currency:
          type: string
          nullable: true
        location:
          type: string
          nullable: true
        price_type:
          type: string
          nullable: true
        availability:
          type: string
          nullable: true
        trade_allowed:
          type: boolean
          nullable: true
        default_condition:
          type: string
          nullable: true
        default_set:
          type: string
          nullable: true
      additionalProperties: false
    MarketListingExtractionContext:
      type: object
      properties:
        candidate_id:
          type: string
          nullable: true
        raw_text:
          type: string
        source_lines:
          type: array
          items:
            type: integer
        source_line_start:
          type: integer
          nullable: true
        source_line_end:
          type: integer
          nullable: true
        intent:
          type: string
          nullable: true
        confidence:
          type: number
          format: double
          nullable: true
        field_sources:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/MarketListingFieldEvidence"
        inherited_context:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/MarketListingFieldEvidence"
        global_context:
          $ref: "#/components/schemas/MarketListingGlobalContext"
        warnings:
          type: array
          items:
            type: string
      additionalProperties: false
    MarketListingItem:
      type: object
      required:
        - id
        - classified_watch_id
        - classification_id
        - watch_index
        - side
        - full_set_status
        - sender
        - source
        - created_at_ms
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        classified_watch_id:
          type: string
          format: uuid
        classification_id:
          type: string
          format: uuid
        watch_index:
          type: integer
        side:
          $ref: "#/components/schemas/MarketSide"
        brand:
          type: string
          nullable: true
        model:
          type: string
          nullable: true
        reference:
          type: string
          nullable: true
        year:
          type: integer
          nullable: true
        price:
          $ref: "#/components/schemas/MarketMoney"
        display_price:
          $ref: "#/components/schemas/MarketMoney"
        price_eur_minor:
          type: integer
          format: int64
          nullable: true
        condition:
          type: string
          nullable: true
          enum:
            - NEW
            - LIKE_NEW
            - USED
        full_set_status:
          type: array
          items:
            $ref: "#/components/schemas/WatchSetComponent"
        dial_type:
          type: string
          nullable: true
        variant:
          type: string
          nullable: true
        sender:
          $ref: "#/components/schemas/SenderSummary"
        source:
          $ref: "#/components/schemas/MarketListingSource"
        price_indicator:
          $ref: "#/components/schemas/MarketPriceIndicator"
        extraction_context:
          $ref: "#/components/schemas/MarketListingExtractionContext"
        created_at_ms:
          type: integer
          format: int64
        updated_at_ms:
          type: integer
          format: int64
      additionalProperties: false
    MarketBySideCounts:
      type: object
      additionalProperties:
        type: integer
        format: int64
      example:
        for_sale: 42
        want_to_buy: 7
    MarketSummary:
      type: object
      required:
        - total
        - by_side
      properties:
        total:
          type: integer
          format: int64
        by_side:
          $ref: "#/components/schemas/MarketBySideCounts"
        min_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        max_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        avg_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        median_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        latest_timestamp_ms:
          type: integer
          format: int64
          nullable: true
      additionalProperties: false
    MarketTrendRequest:
      type: object
      properties:
        view_id:
          type: string
          format: uuid
        filters:
          $ref: "#/components/schemas/MarketFilters"
        bucket:
          type: string
          enum:
            - day
            - week
            - month
          default: week
        metric:
          type: string
          enum:
            - avg_price_eur_minor
            - median_price_eur_minor
            - listing_count
          default: median_price_eur_minor
        from_ms:
          type: integer
          format: int64
        to_ms:
          type: integer
          format: int64
        exclude_outliers:
          type: boolean
          default: false
      additionalProperties: false
    MarketTrendPoint:
      type: object
      required:
        - bucket_start_ms
        - listing_count
        - price_sample_count
      properties:
        bucket_start_ms:
          type: integer
          format: int64
        listing_count:
          type: integer
        price_sample_count:
          type: integer
        avg_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        median_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        metric_value:
          type: number
          format: double
          nullable: true
      additionalProperties: false
    MarketTrendEvidence:
      type: object
      required:
        - endpoint
        - filters_hash
        - from_ms
        - to_ms
      properties:
        endpoint:
          type: string
        filters_hash:
          type: string
        from_ms:
          type: integer
          format: int64
        to_ms:
          type: integer
          format: int64
      additionalProperties: false
    MarketTrendResponse:
      type: object
      required:
        - bucket
        - metric
        - points
        - sample_count
        - confidence
        - warnings
        - evidence
      properties:
        bucket:
          type: string
        metric:
          type: string
        points:
          type: array
          items:
            $ref: "#/components/schemas/MarketTrendPoint"
        start_metric:
          type: number
          format: double
          nullable: true
        end_metric:
          type: number
          format: double
          nullable: true
        absolute_change:
          type: number
          format: double
          nullable: true
        percent_change:
          type: number
          format: double
          nullable: true
        sample_count:
          type: integer
        confidence:
          type: string
          enum:
            - none
            - low
            - moderate
            - high
        warnings:
          type: array
          items:
            type: string
        evidence:
          $ref: "#/components/schemas/MarketTrendEvidence"
      additionalProperties: false
    MarketFacetBucket:
      type: object
      required:
        - value
        - count
      properties:
        value:
          type: string
        count:
          type: integer
          format: int64
      additionalProperties: false
    MarketYearFacetBucket:
      type: object
      required:
        - value
        - count
      properties:
        value:
          type: integer
        count:
          type: integer
          format: int64
      additionalProperties: false
    MarketPriceRangeFacet:
      type: object
      required:
        - currency
      properties:
        min_minor:
          type: integer
          format: int64
          nullable: true
        max_minor:
          type: integer
          format: int64
          nullable: true
        currency:
          type: string
          example: EUR
      additionalProperties: false
    MarketFacetSet:
      type: object
      required:
        - brands
        - conditions
        - years
        - dial_types
        - variants
        - full_set_status
        - country_codes
        - groups
        - price_range
      properties:
        brands:
          type: array
          items:
            $ref: "#/components/schemas/MarketFacetBucket"
        conditions:
          type: array
          items:
            $ref: "#/components/schemas/MarketFacetBucket"
        years:
          type: array
          items:
            $ref: "#/components/schemas/MarketYearFacetBucket"
        dial_types:
          type: array
          items:
            $ref: "#/components/schemas/MarketFacetBucket"
        variants:
          type: array
          items:
            $ref: "#/components/schemas/MarketFacetBucket"
        full_set_status:
          type: array
          items:
            $ref: "#/components/schemas/MarketFacetBucket"
        country_codes:
          type: array
          items:
            $ref: "#/components/schemas/MarketFacetBucket"
        groups:
          type: array
          items:
            $ref: "#/components/schemas/MarketFacetBucket"
        price_range:
          $ref: "#/components/schemas/MarketPriceRangeFacet"
      additionalProperties: false
    MarketEffectiveFilter:
      type: object
      description: Server-normalized effective filter envelope returned for debugging saved-view and request filter composition.
      additionalProperties: true
    MarketListingPage:
      type: object
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/MarketListingItem"
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
        summary:
          $ref: "#/components/schemas/MarketSummary"
        facets:
          $ref: "#/components/schemas/MarketFacetSet"
        effective_filter:
          $ref: "#/components/schemas/MarketEffectiveFilter"
      additionalProperties: false
    MarketReferenceItem:
      type: object
      required:
        - brand
        - reference
        - listing_count
        - for_sale_count
        - want_to_buy_count
        - conditions
        - dial_types
        - variants
      properties:
        brand:
          type: string
        reference:
          type: string
        model:
          type: string
          nullable: true
        listing_count:
          type: integer
        for_sale_count:
          type: integer
        want_to_buy_count:
          type: integer
        min_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        max_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        avg_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        median_price_eur_minor:
          type: integer
          format: int64
          nullable: true
        display_min_price:
          $ref: "#/components/schemas/MarketMoney"
        display_max_price:
          $ref: "#/components/schemas/MarketMoney"
        display_avg_price:
          $ref: "#/components/schemas/MarketMoney"
        display_median_price:
          $ref: "#/components/schemas/MarketMoney"
        sample_media_asset_id:
          type: string
          format: uuid
          nullable: true
        conditions:
          type: array
          items:
            type: string
            enum:
              - NEW
              - LIKE_NEW
              - USED
        dial_types:
          type: array
          items:
            type: string
        variants:
          type: array
          items:
            type: string
        first_seen_at_ms:
          type: integer
          format: int64
          nullable: true
        last_seen_at_ms:
          type: integer
          format: int64
          nullable: true
      additionalProperties: false
    MarketReferencePage:
      type: object
      required:
        - items
        - has_more
        - total
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/MarketReferenceItem"
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
        total:
          type: integer
          format: int64
      additionalProperties: false
    AlertFilters:
      type: object
      description: >-
        Alert filter object. `resource: "messages"` matches canonical message
        text without waiting for classification; `resource: "watch_listings"`
        matches classified market listings. Other keys reuse saved-view filters.
      additionalProperties: true
    AlertItem:
      type: object
      required:
        - id
        - name
        - enabled
        - filters
        - created_at_ms
        - updated_at_ms
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        enabled:
          type: boolean
        view_id:
          type: string
          format: uuid
          nullable: true
        filters:
          $ref: "#/components/schemas/AlertFilters"
        created_by:
          type: string
          nullable: true
        updated_by:
          type: string
          nullable: true
        created_at_ms:
          type: integer
          format: int64
        updated_at_ms:
          type: integer
          format: int64
      additionalProperties: false
    AlertPage:
      type: object
      required:
        - items
        - total
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/AlertItem"
        total:
          type: integer
      additionalProperties: false
    CreateAlertRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        enabled:
          type: boolean
        view_id:
          type: string
          format: uuid
          nullable: true
        filters:
          $ref: "#/components/schemas/AlertFilters"
      additionalProperties: false
    UpdateAlertRequest:
      type: object
      properties:
        name:
          type: string
        enabled:
          type: boolean
        view_id:
          type: string
          format: uuid
          nullable: true
        filters:
          $ref: "#/components/schemas/AlertFilters"
      additionalProperties: false
    AlertMatchItem:
      type: object
      required:
        - alert_id
        - resource
        - resource_id
        - listing_id
        - canonical_message_id
        - matched_at_ms
      properties:
        alert_id:
          type: string
          format: uuid
        resource:
          type: string
          enum:
            - watch_listing
            - message
        resource_id:
          type: string
          format: uuid
          description: Stable id of the matched resource. For message matches this is the canonical message id; for watch listing matches this is the listing id.
        listing_id:
          type: string
          format: uuid
          description: Backwards-compatible alias for the match resource id. For watch listing matches this is the listing id; for message matches this is the canonical message id.
        canonical_message_id:
          type: string
          format: uuid
        matched_at_ms:
          type: integer
          format: int64
        seen_at_ms:
          type: integer
          format: int64
          nullable: true
      additionalProperties: false
    AlertMatchPage:
      type: object
      required:
        - items
        - total
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/AlertMatchItem"
        total:
          type: integer
      additionalProperties: false
    AlertBulkMarkSeenResult:
      type: object
      required:
        - status
        - matched_count
      properties:
        status:
          type: string
          enum:
            - accepted
            - noop
        event_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Omitted for large bulk operations.
        matched_count:
          type: integer
          minimum: 0
          description: Number of unread matches marked as seen.
        request_id:
          type: string
      additionalProperties: false
    DashboardStats:
      type: object
      required:
        - listing_count
        - for_sale_listing_count
        - want_to_buy_listing_count
        - reference_count
        - contact_count
        - group_count
        - selected_group_count
        - alert_count
        - unseen_alert_match_count
        - updated_at_ms
      properties:
        listing_count:
          type: integer
          format: int64
        for_sale_listing_count:
          type: integer
          format: int64
          description: Visible non-duplicate projected for-sale market listings.
        want_to_buy_listing_count:
          type: integer
          format: int64
          description: Visible non-duplicate projected wanted market listings.
        reference_count:
          type: integer
          format: int64
        contact_count:
          type: integer
          format: int64
        group_count:
          type: integer
          format: int64
        selected_group_count:
          type: integer
          format: int64
        alert_count:
          type: integer
          format: int64
        unseen_alert_match_count:
          type: integer
          format: int64
        latest_listing_at_ms:
          type: integer
          format: int64
          nullable: true
        latest_message_at_ms:
          type: integer
          format: int64
          nullable: true
        updated_at_ms:
          type: integer
          format: int64
      additionalProperties: false
    RecentlyViewedResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/SearchResultItem"
      additionalProperties: false
    SearchHistoryResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/SearchHistoryItem"
      additionalProperties: false
    SearchHistoryItem:
      type: object
      required:
        - query
        - searched_at_ms
        - search_count
      properties:
        query:
          type: string
        searched_at_ms:
          type: integer
          format: int64
        search_count:
          type: integer
          minimum: 1
      additionalProperties: false
    RecordSearchHistoryRequest:
      type: object
      required:
        - query
      properties:
        query:
          type: string
          minLength: 1
          maxLength: 256
      additionalProperties: false
    SearchResultItem:
      type: object
      required:
        - id
        - type
        - label
        - href
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - references
            - listings
            - contacts
            - messages
            - alerts
            - views
        label:
          type: string
        subtitle:
          type: string
          nullable: true
        href:
          type: string
        thumbnailUrl:
          type: string
          nullable: true
        preview:
          $ref: "#/components/schemas/SearchPreview"
      additionalProperties: false
    SearchPreview:
      type: object
      properties:
        primary:
          type: string
        secondary:
          type: string
        badge:
          type: string
        tone:
          type: string
          enum:
            - neutral
            - positive
            - warning
            - negative
            - action
      additionalProperties: false
    AgentThreadListResponse:
      type: object
      required:
        - threads
      properties:
        threads:
          type: array
          items:
            $ref: "#/components/schemas/AgentThreadSummary"
      additionalProperties: false
    AgentThreadSummary:
      type: object
      required:
        - remoteId
        - status
        - createdAtMs
        - updatedAtMs
      properties:
        remoteId:
          type: string
          format: uuid
        externalId:
          type: string
          nullable: true
        status:
          type: string
          enum:
            - regular
            - archived
        title:
          type: string
          nullable: true
        createdAtMs:
          type: integer
          format: int64
        updatedAtMs:
          type: integer
          format: int64
      additionalProperties: false
    InitializeAgentThreadRequest:
      type: object
      properties:
        remoteId:
          type: string
          format: uuid
      additionalProperties: false
    InitializeAgentThreadResponse:
      type: object
      required:
        - remoteId
      properties:
        remoteId:
          type: string
          format: uuid
        externalId:
          type: string
          nullable: true
      additionalProperties: false
    PatchAgentThreadRequest:
      type: object
      properties:
        title:
          type: string
          nullable: true
          maxLength: 160
        status:
          type: string
          enum:
            - regular
            - archived
      additionalProperties: false
    AgentThreadHistory:
      type: object
      description: Assistant UI exported thread repository. The `messages` array stores assistant-ui message repository items and may include forward-compatible library-owned fields.
      required:
        - messages
      properties:
        messages:
          type: array
          items:
            $ref: "#/components/schemas/AgentThreadHistoryMessage"
        headId:
          type: string
          nullable: true
      additionalProperties: true
    AgentThreadHistoryMessage:
      type: object
      description: Assistant UI exported message repository item. Its exact shape is owned by assistant-ui and is stored as JSON for thread restoration.
      additionalProperties: true
    WahaFlexibleObject:
      type: object
      description: Provider-owned WAHA object. The backend stores this as raw evidence and extracts only documented normalized fields.
      additionalProperties: true
    WahaWebhookMetadata:
      type: object
      nullable: true
      description: Free-form metadata sent by WAHA. Watchatic currently uses it only as raw evidence.
      properties:
        fixture:
          type: string
          description: Present in local fixtures only.
        watchatic.user_id:
          type: string
          description: Watchatic-owned WAHA session metadata when present.
      additionalProperties: true
    WahaWebhookEnvironment:
      type: object
      nullable: true
      description: WAHA runtime environment details.
      properties:
        version:
          type: string
          example: 2026.4.1
      additionalProperties: true
    WahaWebhookMedia:
      type: object
      nullable: true
      description: WAHA media metadata for message events.
      properties:
        url:
          type: string
          nullable: true
        mimetype:
          type: string
          nullable: true
          example: image/jpeg
        filename:
          type: string
          nullable: true
        s3:
          $ref: "#/components/schemas/WahaFlexibleObject"
      additionalProperties: true
    WahaWebhookPayload:
      type: object
      nullable: true
      description: Raw provider payload for the WAHA event. Common message, edit, revoke, session and group fields are documented; unknown WAHA fields are preserved.
      properties:
        id:
          type: string
          description: WAHA message or payload id when present.
          example: false_120363000@g.us_AAAAA_123@lid
        body:
          type: string
          nullable: true
          example: Rolex 126610LN full set 2023 EUR 12500
        caption:
          type: string
          nullable: true
        from:
          type: string
          nullable: true
          example: 120363000@g.us
        from_:
          type: string
          nullable: true
          description: WAHA alias for `from` in some payloads.
          x-go-name: FromAlias
        to:
          type: string
          nullable: true
        participant:
          type: string
          nullable: true
          example: 31612345678@c.us
        author:
          type: string
          nullable: true
        fromMe:
          type: boolean
          nullable: true
        timestamp:
          type: integer
          format: int64
          nullable: true
        type:
          type: string
          nullable: true
        source:
          type: string
          nullable: true
          example: app
        ack:
          type: integer
          nullable: true
        ackName:
          type: string
          nullable: true
        hasMedia:
          type: boolean
          nullable: true
        media:
          $ref: "#/components/schemas/WahaWebhookMedia"
        mediaUrl:
          type: string
          nullable: true
        location:
          $ref: "#/components/schemas/WahaFlexibleObject"
        replyTo:
          $ref: "#/components/schemas/WahaFlexibleObject"
        vCards:
          type: array
          nullable: true
          items:
            $ref: "#/components/schemas/WahaFlexibleObject"
        _data:
          $ref: "#/components/schemas/WahaFlexibleObject"
        editedMessageId:
          type: string
          nullable: true
        revokedMessageId:
          type: string
          nullable: true
        before:
          $ref: "#/components/schemas/WahaFlexibleObject"
        after:
          $ref: "#/components/schemas/WahaFlexibleObject"
        status:
          type: string
          nullable: true
        me:
          $ref: "#/components/schemas/WahaFlexibleObject"
        group:
          $ref: "#/components/schemas/WahaFlexibleObject"
        participants:
          type: array
          nullable: true
          items:
            $ref: "#/components/schemas/WahaFlexibleObject"
      additionalProperties: true
    WahaWebhookEnvelope:
      type: object
      description: Raw WAHA webhook envelope. It is intentionally flexible because WAHA can add event-specific top-level fields; stable fields are still documented here.
      required:
        - id
        - event
        - session
      additionalProperties: true
      properties:
        id:
          type: string
          description: WAHA event ULID.
        timestamp:
          type: integer
          format: int64
          nullable: true
          description: WAHA event creation timestamp in Unix milliseconds when present.
        event:
          type: string
          enum:
            - session.status
            - message
            - message.reaction
            - message.any
            - message.ack
            - message.ack.group
            - message.waiting
            - message.revoked
            - message.edited
            - state.change
            - group.join
            - group.leave
            - group.v2.join
            - group.v2.leave
            - group.v2.update
            - group.v2.participants
            - presence.update
            - poll.vote
            - poll.vote.failed
            - chat.archive
            - call.received
            - call.accepted
            - call.rejected
            - label.upsert
            - label.deleted
            - label.chat.added
            - label.chat.deleted
            - event.response
            - event.response.failed
            - engine.event
        session:
          type: string
        engine:
          type: string
          enum:
            - WEBJS
            - WPP
            - NOWEB
            - GOWS
          nullable: true
        metadata:
          $ref: "#/components/schemas/WahaWebhookMetadata"
        payload:
          $ref: "#/components/schemas/WahaWebhookPayload"
        environment:
          $ref: "#/components/schemas/WahaWebhookEnvironment"
      example:
        id: evt_123
        timestamp: 1777803300000
        event: message
        session: user-user_35eh25qkkIVwab803vgofO1uSsl-receiving
        engine: WEBJS
        metadata: {}
        payload:
          id: false_120363000@g.us_AAAAA_123@lid
          body: Rolex 126610LN full set 2023 EUR 12500
