openapi: 3.1.0
info:
  title: AHS Platform API
  version: 0.23.0
  description: Foundation, identity, tenancy, knowledge ingestion, retrieval, and grounded playground contract for Arabic Help Systems.
servers:
  - url: http://localhost:8080
    description: Local development
tags:
  - name: Operations
  - name: System
  - name: Identity
  - name: Organizations
  - name: Projects
  - name: Audit
  - name: Knowledge
  - name: Playground
  - name: Conversations
  - name: Widget
  - name: API Keys
  - name: Answers
  - name: Billing
paths:
  /healthz:
    get:
      operationId: getLiveness
      tags: [Operations]
      summary: Report process liveness
      responses:
        '200':
          description: The API process is alive.
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Liveness'
  /readyz:
    get:
      operationId: getReadiness
      tags: [Operations]
      summary: Report dependency readiness
      responses:
        '200':
          description: Every required dependency is ready.
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FoundationStatus'
        '503':
          description: At least one required dependency is unavailable.
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FoundationStatus'
  /v1/system/ping:
    get:
      operationId: pingSystem
      tags: [System]
      summary: Exercise the HTTP gateway and foundation service
      security: []
      responses:
        '200':
          description: The core foundation and its required dependencies are ready.
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: '#/components/schemas/FoundationStatus'
        '503':
          description: A required dependency is unavailable.
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /v1/auth/email-otp/request:
    post:
      operationId: requestEmailOTP
      tags: [Identity]
      summary: Send a passwordless sign-in code without disclosing account existence
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailOTPRequest'
      responses:
        '202':
          description: The code was accepted for delivery.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailOTPChallengeEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/auth/email-otp/verify:
    post:
      operationId: verifyEmailOTP
      tags: [Identity]
      summary: Verify a passwordless sign-in code
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailOTPVerifyRequest'
      responses:
        '200':
          description: Existing users receive a session; new users receive a short-lived profile-completion token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailOTPVerifyEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/auth/email-otp/complete-profile:
    post:
      operationId: completeEmailOTPProfile
      tags: [Identity]
      summary: Create a new verified user after OTP verification
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmailOTPProfileRequest'
      responses:
        '201':
          description: User and session created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/auth/logout:
    post:
      operationId: logout
      tags: [Identity]
      summary: Revoke the active session
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204':
          description: Session revoked and cookies cleared.
        default:
          $ref: '#/components/responses/Error'
  /v1/auth/session:
    get:
      operationId: getCurrentSession
      tags: [Identity]
      summary: Return the authenticated user and session expiry
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Active session.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations:
    get:
      operationId: listOrganizations
      tags: [Organizations]
      summary: List organizations for the authenticated user
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Organization memberships.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrganizationListEnvelope'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createOrganization
      tags: [Organizations]
      summary: Create an organization with the caller as owner
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrganizationRequest'
      responses:
        '201':
          description: Organization created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrganizationEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations/{organization_id}:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
    delete:
      operationId: deleteOrganization
      tags: [Organizations]
      summary: Permanently delete an organization after email OTP confirmation
      description: Owner-only and irreversible. Deletes all projects, environments, knowledge, API keys, memberships, billing records, and complete client conversation histories including messages and citations. The user account remains. An externally active paid subscription must be canceled first. Requires a purpose-scoped deletion challenge, the emailed code, and the exact organization name.
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeleteOrganizationRequest'
      responses:
        '204':
          description: The organization and all tenant-owned data were permanently deleted.
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations/{organization_id}/deletion-challenges:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
    post:
      operationId: requestOrganizationDeletionOTP
      tags: [Organizations]
      summary: Email an organization deletion confirmation code to the owner
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrganizationDeletionChallengeRequest'
      responses:
        '202':
          description: A short-lived deletion challenge was created and its code sent to the owner's account email.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrganizationDeletionChallengeEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations/{organization_id}/members:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
    get:
      operationId: listOrganizationMembers
      tags: [Organizations]
      summary: List organization members when the caller can manage members
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Organization members.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemberListEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations/{organization_id}/members/{user_id}:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
      - $ref: '#/components/parameters/UserID'
    patch:
      operationId: updateOrganizationMemberRole
      tags: [Organizations]
      summary: Change a non-owner member role within the support-agent seat entitlement
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateMemberRoleRequest'
      responses:
        '200':
          description: Member role updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemberEnvelope'
        '409':
          description: Changing this member to Support Agent would exceed the organization's current agent-seat entitlement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: removeOrganizationMember
      tags: [Organizations]
      summary: Remove a non-owner organization member
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204':
          description: Member removed.
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations/{organization_id}/support-agents:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
    get:
      operationId: listOrganizationSupportAgents
      tags: [Organizations, Conversations]
      summary: List assignable Support Agents
      description: Requires `conversations.assign` and returns only current Support Agent members from the exact organization. It does not expose members in other roles.
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Current assignable Support Agents.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemberListEnvelope'
        '403':
          description: The caller cannot assign conversations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations/{organization_id}/invitations:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
    get:
      operationId: listOrganizationInvitations
      tags: [Organizations]
      summary: List active organization invitations
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Active invitations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvitationListEnvelope'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: inviteOrganizationMember
      tags: [Organizations]
      summary: Send or replace an organization invitation
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInvitationRequest'
      responses:
        '201':
          description: Invitation created and sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvitationEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/invitations/accept:
    post:
      operationId: acceptOrganizationInvitation
      tags: [Organizations]
      summary: Accept an invitation matching the authenticated email and current seat entitlement
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        $ref: '#/components/requestBodies/TokenRequest'
      responses:
        '200':
          description: Invitation accepted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemberEnvelope'
        '409':
          description: Accepting a Support Agent invitation would exceed the organization's current agent-seat entitlement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations/{organization_id}/projects:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
    get:
      operationId: listOrganizationProjects
      tags: [Projects]
      summary: List projects and their isolated environments
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Tenant-scoped projects.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectListEnvelope'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createOrganizationProject
      tags: [Projects]
      summary: Create a project with Test and Production environments
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProjectRequest'
      responses:
        '201':
          description: Project and environments created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectEnvelope'
        '409':
          description: The organization's current project entitlement is exhausted or its subscription cannot create projects.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations/{organization_id}/projects/{project_id}:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
      - $ref: '#/components/parameters/ProjectIDPath'
    delete:
      operationId: deleteOrganizationProject
      tags: [Projects]
      summary: Permanently delete one project after email OTP confirmation
      description: Owner-only and irreversible. Deletes the selected project's environments, knowledge, publishable Widget keys, conversations, messages, and citations while leaving the organization and other projects intact. Requires a project-scoped deletion challenge, the emailed code, and the exact project name.
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeleteOrganizationRequest'
      responses:
        '204':
          description: The project and its project-owned data were permanently deleted.
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations/{organization_id}/projects/{project_id}/deletion-challenges:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
      - $ref: '#/components/parameters/ProjectIDPath'
    post:
      operationId: requestProjectDeletionOTP
      tags: [Projects]
      summary: Email a project deletion confirmation code to the organization owner
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrganizationDeletionChallengeRequest'
      responses:
        '202':
          description: A short-lived project deletion challenge was created and its code sent to the owner's account email.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrganizationDeletionChallengeEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations/{organization_id}/audit-events:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
    get:
      operationId: listOrganizationAuditEvents
      tags: [Audit]
      summary: List recent tenant audit events
      security:
        - sessionCookie: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: Recent audit events.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditEventListEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/knowledge/sources:
    get:
      operationId: listKnowledgeSources
      tags: [Knowledge]
      summary: List knowledge sources in an authorized project environment
      description: Requires the `knowledge.view` permission. The organization is derived from the authenticated membership and project.
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/ProjectIDQuery'
        - $ref: '#/components/parameters/EnvironmentIDQuery'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/PageLimit'
      responses:
        '200':
          description: Cursor-paginated sources for exactly one project environment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSourceListEnvelope'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: uploadKnowledgeSource
      tags: [Knowledge]
      summary: Store, parse, and chunk a knowledge source
      description: Requires `knowledge.manage`. The configured file limit defaults to 10 MiB, while the organization-wide knowledge-byte entitlement counts every stored immutable source version. The entitlement check and metadata insert are atomic; a rejected object upload is removed. PDF, DOCX, and UTF-8 text bytes are accepted; Markdown is detected as text/plain because it has no distinct byte signature. Poppler performs bounded PDF inspection and native extraction. Arabic or image-only/scanned PDFs use bounded local Tesseract OCR with Arabic and English models so positioned Arabic glyphs are never trusted as logical text. Parsing is deadline/cancellation bounded, limited to 200 pages and 4 MiB extracted text by default, and runs without a network call. Successful extraction moves through `parsed` and `chunking` to `chunked`. Chunk commit transactionally queues provider-neutral indexing; the response never waits for the embedding provider. Parser, OCR, or chunker rejection is persisted as an observable `failed` source with a stable reason.
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              additionalProperties: false
              required: [project_id, environment_id, file]
              properties:
                project_id:
                  type: string
                  format: uuid
                environment_id:
                  type: string
                  format: uuid
                file:
                  type: string
                  format: binary
                  description: Non-empty file whose actual bytes are at most the configured upload limit.
      responses:
        '201':
          description: The object and immutable metadata version were created, then ingestion reached `chunked` with durable indexing queued, or an observable ingestion `failed` state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSourceEnvelope'
        '409':
          description: The file would exceed the organization's current knowledge-storage entitlement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '413':
          description: The file exceeds the configured upload limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '415':
          description: The detected bytes are not a supported knowledge format.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                unsupportedType:
                  summary: The detected bytes are not a supported knowledge format.
                  value:
                    error:
                      code: unsupported_knowledge_file_type
                      message: The uploaded file type is not supported.
                      request_id: req_example
        default:
          $ref: '#/components/responses/Error'
  /v1/knowledge/search:
    post:
      operationId: searchKnowledge
      tags: [Knowledge]
      summary: Search active indexed knowledge with exact scoped cosine ranking
      description: Requires `knowledge.view`, a valid CSRF token, and an enabled production embedding profile. The query is embedded as a retrieval query and searched only against active, ready chunks matching the authorized organization, requested project/environment, and identical internal embedding profile. The response is provider-neutral. Search is exact; no ANN index or global pre-ranking is used.
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KnowledgeSearchRequest'
      responses:
        '200':
          description: Best matching active chunks in descending exact cosine similarity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSearchEnvelope'
        '404':
          description: The project/environment scope is outside every tenant visible to the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: The authenticated user exceeded the bounded search rate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '503':
          description: Production query embedding is disabled or temporarily unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/playground/answers:
    post:
      operationId: createPlaygroundAnswer
      tags: [Playground]
      summary: Generate one grounded test answer from authorized project knowledge
      description: Requires `knowledge.view`, a valid CSRF token, enabled query embedding and answer generation, and their separate paid-usage confirmations. Retrieval is restricted to the authenticated user's requested project/environment before bounded evidence reaches generation. A successful grounded or uncertain result is persisted atomically as an environment-scoped conversation with customer/assistant messages and exact trusted citation references before the response is returned; persistence failure fails closed. The response remains provider-neutral and does not expose conversation identifiers, model identifiers, token usage, provider errors, prompts, or credentials.
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlaygroundAnswerRequest'
      responses:
        '200':
          description: A grounded answer with trusted citations, or a localized uncertainty answer suggesting handoff.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlaygroundAnswerEnvelope'
        '403':
          description: The caller lacks knowledge-view permission or the CSRF token is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The project/environment scope is outside every tenant visible to the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The request body, identifiers, question, or language is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: The authenticated user exceeded the bounded playground-answer rate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '503':
          description: Grounded retrieval or answer generation is disabled, timed out, or temporarily unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/playground/answers/stream:
    post:
      operationId: streamPlaygroundAnswer
      tags: [Playground]
      summary: Stream one verified grounded test answer over SSE
      description: Uses the same authorization, CSRF, paid-work rate limit, retrieval, generation, citation validation, and atomic conversation persistence as the JSON endpoint. AHS deliberately validates and persists the complete answer before sending the first answer byte. The provider-neutral SSE sequence is exactly one `answer_start`, one or more UTF-8-safe `answer_delta` events, then one `answer_complete` carrying trusted citations. Provider/model/usage/prompt fields are never emitted. Errors before streaming begins use the normal JSON error envelope and HTTP status.
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlaygroundAnswerRequest'
      responses:
        '200':
          description: Ordered Server-Sent Events containing the verified persisted answer and trusted citations.
          content:
            text/event-stream:
              schema:
                type: string
              example: |-
                event: answer_start
                data: {"status":"grounded","handoff_suggested":false}

                event: answer_delta
                data: {"text":"يمكن الإرجاع خلال "}

                event: answer_delta
                data: {"text":"14 يومًا."}

                event: answer_complete
                data: {"citations":[]}
        '403':
          description: The caller lacks knowledge-view permission or the CSRF token is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The project/environment scope is outside every tenant visible to the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The request body, identifiers, question, or language is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: The authenticated user exceeded the shared playground-answer rate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '503':
          description: Grounded retrieval or answer generation is disabled, timed out, or temporarily unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/answers:
    post:
      operationId: createServerAnswer
      tags: [Answers]
      summary: Create a grounded answer from a trusted backend
      description: Authenticates an environment-bound server secret from `X-AHS-API-Key` without requiring a browser Origin. If the key has an IP allowlist, the source public IP must match exactly. Server secrets presented by a browser request carrying Origin are rejected and must never be embedded in browser or mobile code.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ServerAnswerRequest'
      responses:
        '200':
          description: Grounded provider-neutral answer persisted as an API conversation.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerAnswerEnvelope'
        '401':
          description: The server key is missing, invalid, revoked, used from a browser Origin, or outside its optional IP allowlist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The question or language is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: The key or source IP exceeded its answer rate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '503':
          description: Grounded retrieval or generation is unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/projects/{project_id}/environments/{environment_id}/publishable-keys:
    parameters:
      - $ref: '#/components/parameters/ProjectIDPath'
      - $ref: '#/components/parameters/EnvironmentIDPath'
    get:
      operationId: listPublishableKeys
      tags: [API Keys]
      summary: List restricted publishable keys for one project environment
      description: Requires `publishable_keys.manage`. Returns metadata and the non-secret display prefix only; full key material is never returned after creation.
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Publishable key metadata for the exact authorized environment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublishableKeyListEnvelope'
        '403':
          description: The caller lacks publishable-key management permission.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The project/environment is outside the caller's authorized tenants.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createPublishableKey
      tags: [API Keys]
      summary: Create a restricted origin-bound publishable key
      description: Requires session, CSRF, and `publishable_keys.manage`. The complete recognizable key is returned exactly once and stored only as a SHA-256 hash. It is permanently bound to this project/environment and one to ten exact HTTPS origins; loopback HTTP is accepted for local development only.
      security:
        - sessionCookie: []
          csrfHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePublishableKeyRequest'
      responses:
        '201':
          description: Created metadata plus the one-time complete publishable key.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedPublishableKeyEnvelope'
        '403':
          description: The caller lacks permission or CSRF validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The project/environment is outside the caller's authorized tenants.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The name or allowed origins are invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/projects/{project_id}/environments/{environment_id}/assistant-settings:
    parameters:
      - $ref: '#/components/parameters/ProjectIDPath'
      - $ref: '#/components/parameters/EnvironmentIDPath'
    get:
      operationId: getAssistantEnvironmentSettings
      tags: [Projects]
      summary: Read provider-neutral response presentation settings for one environment
      security: [{ sessionCookie: [] }]
      responses:
        '200':
          description: Effective settings, including defaults when no override has been saved.
          content:
            {
              application/json:
                {
                  schema:
                    { $ref: '#/components/schemas/AssistantSettingsEnvelope' }
                }
            }
        default: { $ref: '#/components/responses/Error' }
    patch:
      operationId: updateAssistantEnvironmentSettings
      tags: [Projects]
      summary: Update response presentation settings for one environment
      description: Requires `assistant.manage`. Presentation preferences never override grounding, citations, uncertainty, restricted-advice, or safety rules.
      security: [{ sessionCookie: [], csrfHeader: [] }]
      requestBody:
        required: true
        content:
          {
            application/json:
              {
                schema: { $ref: '#/components/schemas/AssistantSettingsInput' }
              }
          }
      responses:
        '200':
          description: Saved settings.
          content:
            {
              application/json:
                {
                  schema:
                    { $ref: '#/components/schemas/AssistantSettingsEnvelope' }
                }
            }
        default: { $ref: '#/components/responses/Error' }
  /v1/projects/{project_id}/environments/{environment_id}/publishable-keys/{key_id}:
    parameters:
      - $ref: '#/components/parameters/ProjectIDPath'
      - $ref: '#/components/parameters/EnvironmentIDPath'
      - $ref: '#/components/parameters/PublishableKeyID'
    delete:
      operationId: revokePublishableKey
      tags: [API Keys]
      summary: Immediately revoke one exact publishable key
      security:
        - sessionCookie: []
          csrfHeader: []
      responses:
        '204':
          description: The key is revoked; repeated revocation remains successful while the row exists.
        '403':
          description: The caller lacks permission or CSRF validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The exact key/scope is not visible to the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/projects/{project_id}/environments/{environment_id}/server-keys:
    parameters:
      - $ref: '#/components/parameters/ProjectIDPath'
      - $ref: '#/components/parameters/EnvironmentIDPath'
    get:
      operationId: listServerKeys
      tags: [API Keys]
      summary: List server secret key metadata for one project environment
      description: Requires `publishable_keys.manage`. Only prefixes, optional allowed IP addresses, and non-secret metadata are returned.
      security: [{ sessionCookie: [] }]
      responses:
        '200':
          description: Server key metadata for the exact authorized environment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerKeyListEnvelope'
        default: { $ref: '#/components/responses/Error' }
    post:
      operationId: createServerKey
      tags: [API Keys]
      summary: Create a server secret key
      description: Requires session, CSRF, and `publishable_keys.manage`. The complete secret is returned exactly once and stored only as a SHA-256 hash. The IP allowlist is optional and accepts exact IPv4 or IPv6 addresses.
      security: [{ sessionCookie: [], csrfHeader: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateServerKeyRequest'
      responses:
        '201':
          description: Created metadata plus the one-time server secret.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedServerKeyEnvelope'
        '422':
          description: The name or one of the exact IP addresses is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default: { $ref: '#/components/responses/Error' }
  /v1/projects/{project_id}/environments/{environment_id}/server-keys/{key_id}:
    parameters:
      - $ref: '#/components/parameters/ProjectIDPath'
      - $ref: '#/components/parameters/EnvironmentIDPath'
      - $ref: '#/components/parameters/PublishableKeyID'
    delete:
      operationId: revokeServerKey
      tags: [API Keys]
      summary: Immediately revoke one exact server key
      security: [{ sessionCookie: [], csrfHeader: [] }]
      responses:
        '204':
          description: The server key is revoked.
        default: { $ref: '#/components/responses/Error' }
  /v1/widget/bootstrap:
    get:
      operationId: bootstrapWidget
      tags: [Widget]
      summary: Resolve safe widget configuration from an origin-bound publishable key
      security:
        - publishableKey: []
      responses:
        '200':
          description: Safe project display configuration; tenant and environment identifiers remain internal.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetBootstrapEnvelope'
        '401':
          description: The key is malformed, revoked, unknown, or not allowed for the exact Origin.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: The publishable key exceeded its bootstrap rate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/widget/answers/stream:
    post:
      operationId: streamWidgetAnswer
      tags: [Widget]
      summary: Create and stream a scoped customer support exchange
      description: Authenticates a restricted publishable key against the exact browser Origin, applies per-key and per-key/visitor rates, retrieves only its immutable tenant/project/environment scope, validates the complete answer and citations, and atomically persists a `web` conversation before emitting provider-neutral SSE. Public citations contain only the stable citation label and source filename.
      security:
        - publishableKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WidgetAnswerRequest'
      responses:
        '200':
          description: Ordered `answer_start`, UTF-8-safe `answer_delta`, and trusted `answer_complete` SSE events. The terminal event returns an opaque `conversation_token`, `last_sequence`, and `assistant_message_sequence`; the hosted Widget may retain the token only in same-tab session storage and sends it only in `X-AHS-Conversation-Token`.
          content:
            text/event-stream:
              schema:
                type: string
        '401':
          description: The key is malformed, revoked, unknown, or not allowed for the exact Origin.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The question or language is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: The publishable key or visitor exceeded its rate, or the organization exhausted its monthly AI-message entitlement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '503':
          description: Grounded retrieval or generation is disabled or unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/widget/conversation/messages:
    get:
      operationId: listWidgetConversationMessages
      tags: [Widget]
      summary: Poll new human-agent messages for one widget conversation
      description: Re-authenticates the publishable key and exact Origin, then verifies the SHA-256 hash of the opaque conversation token. It returns only public-safe agent messages after the caller cursor and never exposes the internal conversation or session identifier.
      security:
        - publishableKey: []
          conversationToken: []
      parameters:
        - name: after_sequence
          in: query
          required: false
          schema:
            type: integer
            format: int64
            minimum: 0
            default: 0
      responses:
        '200':
          description: Current public conversation status plus ordered agent messages after the cursor.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetConversationMessagePageEnvelope'
        '401':
          description: The publishable key, exact Origin, or conversation session is invalid, revoked, or expired.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The cursor is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: The publishable key or visitor exceeded its polling rate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/widget/conversation:
    get:
      operationId: getWidgetConversationHistory
      tags: [Widget]
      summary: Restore one verified Widget conversation timeline
      description: Re-authenticates the publishable key, exact Origin, and hashed conversation session. Returns a bounded page of public-safe customer, assistant, and agent messages plus assistant citation labels and current feedback. Internal identifiers, agent identity, provider state, prompts, evidence text, and system messages remain private.
      security:
        - publishableKey: []
          conversationToken: []
      parameters:
        - name: after_sequence
          in: query
          required: false
          schema:
            type: integer
            format: int64
            minimum: 0
            default: 0
      responses:
        '200':
          description: A bounded public-safe timeline page for the exact verified conversation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetConversationHistoryEnvelope'
        '401':
          description: The publishable key, exact Origin, or conversation session is invalid, revoked, or expired.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The cursor is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: The publishable key or visitor exceeded its history rate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/widget/conversation/feedback:
    put:
      operationId: putWidgetConversationFeedback
      tags: [Widget]
      summary: Set feedback for an assistant message in the verified conversation
      description: Resolves the assistant message by sequence only inside the exact origin/key/session-bound conversation. Repeating or changing the choice atomically upserts one durable feedback row.
      security:
        - publishableKey: []
          conversationToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WidgetFeedbackInput'
      responses:
        '200':
          description: The current feedback choice was persisted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetFeedbackEnvelope'
        '401':
          description: The publishable key, exact Origin, or conversation session is invalid, revoked, or expired.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The assistant message does not exist in this conversation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The sequence or rating is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: The publishable key or visitor exceeded its feedback rate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createWidgetCustomerMessage
      tags: [Widget]
      summary: Append a customer follow-up to the active human-support conversation
      description: Re-authenticates the publishable key, exact Origin, and hashed conversation session, then atomically appends the next customer message. It is accepted only while handoff is requested, queued, human-active, or waiting for the customer. A waiting conversation resumes human-active support. No AI provider is called.
      security:
        - publishableKey: []
          conversationToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WidgetCustomerMessageRequest'
      responses:
        '201':
          description: The bounded customer message was appended to the same ordered conversation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetCustomerMessageResultEnvelope'
        '401':
          description: The publishable key, exact Origin, or conversation session is invalid, revoked, or expired.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The message is invalid or the conversation state cannot accept a customer follow-up.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: The publishable key or visitor exceeded its customer-message rate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/organizations/{organization_id}/billing:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
    get:
      operationId: getBillingOverview
      tags: [Billing]
      summary: Read provider-neutral subscription, entitlement, plan, and monthly usage state
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Billing overview for one authorized organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingOverviewEnvelope'
        '404':
          description: The organization is outside the caller's memberships.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        default: { $ref: '#/components/responses/Error' }
  /v1/organizations/{organization_id}/billing/checkout:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
    post:
      operationId: createBillingCheckout
      tags: [Billing]
      summary: Create a short-lived Stripe-hosted subscription Checkout session
      description: Owner, Admin, or Billing role plus CSRF is required. The server chooses the configured Stripe Price ID from the provider-neutral plan ID; secret keys and provider IDs are never returned.
      security:
        - sessionCookie: []
          csrfHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [plan_id]
              properties:
                plan_id: { type: string, enum: [growth, business] }
      responses:
        '201':
          description: A validated HTTPS redirect to Stripe-hosted Checkout.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BillingRedirectEnvelope' }
        '403':
          description: Permission or CSRF validation failed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '503':
          description: Stripe billing or the selected plan price is not configured.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        default: { $ref: '#/components/responses/Error' }
  /v1/organizations/{organization_id}/billing/portal:
    parameters:
      - $ref: '#/components/parameters/OrganizationID'
    post:
      operationId: createBillingPortal
      tags: [Billing]
      summary: Create a Stripe-hosted customer Billing Portal session
      security:
        - sessionCookie: []
          csrfHeader: []
      responses:
        '201':
          description: A validated HTTPS redirect to Stripe-hosted Billing Portal.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BillingRedirectEnvelope' }
        '403':
          description: Permission or CSRF validation failed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '404':
          description: No provider customer exists for this organization.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        default: { $ref: '#/components/responses/Error' }
  /v1/billing/webhooks/stripe:
    post:
      operationId: receiveStripeBillingWebhook
      tags: [Billing]
      summary: Verify and idempotently apply Stripe subscription lifecycle events
      description: Reads at most 1 MiB of the untouched request body, verifies the Stripe-Signature HMAC and five-minute timestamp tolerance, ignores unrelated signed events, and applies only tenant-tagged subscription events monotonically.
      security: []
      responses:
        '200':
          description: The signed event was applied, already applied, stale, or intentionally ignored.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [received]
                properties:
                  received: { type: boolean, const: true }
        '400':
          description: The payload, timestamp, signature, tenant metadata, plan, or status is invalid.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '503':
          description: Billing is disabled.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        default: { $ref: '#/components/responses/Error' }
  /v1/conversations:
    get:
      operationId: listConversations
      tags: [Conversations]
      summary: List conversation metadata in one authorized project environment
      description: Requires `conversations.view` for Owner, Admin, and Support Manager. A Support Agent receives only conversations currently assigned to that exact user through `conversations.view_assigned`. The opaque cursor is bound to the exact project/environment. Message bodies and citation metadata are excluded from the collection response.
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/ProjectIDQuery'
        - $ref: '#/components/parameters/EnvironmentIDQuery'
        - $ref: '#/components/parameters/ConversationCursor'
        - $ref: '#/components/parameters/ConversationPageLimit'
      responses:
        '200':
          description: Cursor-paginated conversation metadata ordered by latest activity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationListEnvelope'
        '400':
          description: A required project or environment identifier is missing or malformed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: The caller lacks conversation-view permission.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The project/environment scope is outside every tenant visible to the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: A query field, limit, or scope-bound cursor is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/conversations/{conversation_id}:
    parameters:
      - $ref: '#/components/parameters/ConversationID'
    get:
      operationId: getConversation
      tags: [Conversations]
      summary: Get one authorized conversation timeline
      description: Requires `conversations.view`, or an exact current assignment for a Support Agent with `conversations.view_assigned`. Returns immutable messages in sequence order and resolves each assistant citation to trusted source/chunk metadata. An identifier outside every tenant or assignment visible to the caller returns 404.
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Conversation metadata and its ordered immutable timeline.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationDetailEnvelope'
        '400':
          description: The conversation identifier is malformed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: The conversation exists in the caller's tenant but the role lacks conversation-view permission.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The conversation is outside every tenant visible to the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/conversations/{conversation_id}/assignment:
    parameters:
      - $ref: '#/components/parameters/ConversationID'
    put:
      operationId: assignConversation
      tags: [Conversations]
      summary: Assign a conversation to one Support Agent
      description: Requires `conversations.assign`, a session, and CSRF. The assignee must currently be a Support Agent in the same organization. Reassignment replaces the single current assignment atomically and writes a content-free audit event.
      security:
        - sessionCookie: []
          csrfHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConversationAssignmentRequest'
      responses:
        '200':
          description: Current assignment after the atomic mutation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationAssignmentEnvelope'
        '400':
          description: The body or conversation identifier is malformed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: The caller lacks assignment permission or the CSRF token is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The conversation is outside every tenant visible to the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The assignee is not a current Support Agent in the conversation organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: unassignConversation
      tags: [Conversations]
      summary: Remove the current conversation assignment
      description: Requires `conversations.assign`, a session, and CSRF. The operation is idempotent for an existing authorized conversation and records a content-free audit event.
      security:
        - sessionCookie: []
          csrfHeader: []
      responses:
        '204':
          description: The conversation has no current assignment.
        '403':
          description: The caller lacks assignment permission or the CSRF token is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The conversation is outside every tenant visible to the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/conversations/{conversation_id}/handoff:
    parameters:
      - $ref: '#/components/parameters/ConversationID'
    post:
      operationId: transitionConversationHandoff
      tags: [Conversations]
      summary: Apply one validated human-handoff action
      description: Requires a session and CSRF. Owner, Admin, and Support Manager may manage visible conversations; Support Agents may transition only their own current assignments. Actions follow the locked lifecycle and cannot skip states. Accept requires a current assignment. Each successful transition updates the conversation and writes a content-free audit event atomically.
      security:
        - sessionCookie: []
          csrfHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConversationHandoffRequest'
      responses:
        '200':
          description: Current handoff state after the transition.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationHandoffEnvelope'
        '400':
          description: The body or conversation identifier is malformed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: The caller lacks permission or the CSRF token is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The conversation is outside the caller's visible tenant or assignment scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The action is not valid from the current state or acceptance has no assignment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/conversations/{conversation_id}/messages:
    parameters:
      - $ref: '#/components/parameters/ConversationID'
    post:
      operationId: createConversationAgentMessage
      tags: [Conversations]
      summary: Append an agent reply to an active human conversation
      description: Requires a session and CSRF. Managers may reply to visible conversations; Support Agents may reply only to their own current assignment. The conversation must be human_active or waiting_customer. The immutable ordered agent message, waiting_customer status, activity timestamps, and content-free audit commit atomically.
      security:
        - sessionCookie: []
          csrfHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateConversationAgentMessageRequest'
      responses:
        '201':
          description: The persisted ordered agent message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationMessageEnvelope'
        '400':
          description: The body or identifier is malformed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: The caller lacks permission or the CSRF token is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: The conversation is outside the caller's visible tenant or assignment scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: The message is invalid or the conversation is not in a human reply state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/knowledge/sources/{source_id}:
    parameters:
      - $ref: '#/components/parameters/SourceID'
    get:
      operationId: getKnowledgeSource
      tags: [Knowledge]
      summary: Get one authorized knowledge source
      description: Requires the `knowledge.view` permission. Object-storage keys are internal and are not returned.
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Source detail and active immutable version metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSourceEnvelope'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteKnowledgeSource
      tags: [Knowledge]
      summary: Permanently delete one authorized knowledge source
      description: Requires `knowledge.manage`. Deletes all immutable versions, stored objects, extracted text, chunks, embeddings, and citation links to this source. Existing conversation message bodies remain, but their links to the deleted evidence are removed. This action cannot be undone.
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204':
          description: The source and its derived knowledge data were permanently deleted.
        default:
          $ref: '#/components/responses/Error'
  /v1/knowledge/sources/{source_id}/versions:
    parameters:
      - $ref: '#/components/parameters/SourceID'
    get:
      operationId: listKnowledgeSourceVersions
      tags: [Knowledge]
      summary: List retained immutable versions for one source
      description: Requires `knowledge.view`. Results are ordered by descending version number. The opaque cursor is bound to this source. Object keys, extracted text, vectors, provider metadata, and creator identity are not returned.
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/PageLimit'
      responses:
        '200':
          description: Cursor-paginated immutable version metadata with active and pending state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSourceVersionListEnvelope'
        '404':
          description: The source is not visible in any authorized tenant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: replaceKnowledgeSource
      tags: [Knowledge]
      summary: Create and process an immutable replacement version
      description: Requires session, CSRF, and `knowledge.manage`. Every retained immutable version counts toward the organization-wide knowledge-byte entitlement, and the entitlement check plus version insert are atomic. The current active version remains available to retrieval while the replacement is parsed, chunked, and indexed. Exactly one pending replacement is allowed. Successful embedding atomically changes the active version and source filename; any ingestion or indexing failure leaves the previous active version unchanged.
      security:
        - sessionCookie: []
          csrfHeader: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              additionalProperties: false
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '201':
          description: The immutable replacement was created and reached a queued indexing or observable failed state; active_version is unchanged and pending_version describes the replacement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSourceEnvelope'
        '409':
          description: Another non-terminal replacement exists, or the replacement would exceed the organization's current knowledge-storage entitlement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '413':
          description: The replacement exceeds the configured upload limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '415':
          description: The replacement bytes are not a supported knowledge format.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/knowledge/sources/{source_id}/active-version:
    parameters:
      - $ref: '#/components/parameters/SourceID'
    put:
      operationId: activateKnowledgeSourceVersion
      tags: [Knowledge]
      summary: Atomically activate one retained ready version
      description: Requires session, CSRF, and `knowledge.manage`. The selected version must belong to this source and be fully chunked and semantically indexed. A non-terminal replacement blocks activation so a background completion cannot overwrite the explicit choice. Repeating the current active version is an idempotent success.
      security:
        - sessionCookie: []
          csrfHeader: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ActivateKnowledgeSourceVersionInput'
      responses:
        '200':
          description: The source with its selected active immutable version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSourceEnvelope'
        '404':
          description: The source or selected version is not visible in the authorized tenant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: The selected version is not fully indexed, or a replacement is still being processed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/knowledge/sources/{source_id}/preview:
    parameters:
      - $ref: '#/components/parameters/SourceID'
    get:
      operationId: previewKnowledgeSource
      tags: [Knowledge]
      summary: Preview bounded extracted text and parsing metadata
      description: Requires `knowledge.view`. Returns at most the first 16,000 Unicode characters of the active version's extracted text. An identifier outside every tenant visible to the caller returns 404. Stored, parsing, or chunking versions return 409; failed versions remain inspectable with their stable failure reason and retain extracted text when parsing succeeded before chunking failed.
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Bounded extracted-text preview or observable parsing failure metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSourcePreviewEnvelope'
        '404':
          description: The source is not visible in any authorized tenant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: The source is still stored, parsing, or chunking.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
  /v1/knowledge/sources/{source_id}/chunks:
    parameters:
      - $ref: '#/components/parameters/SourceID'
    get:
      operationId: listKnowledgeSourceChunks
      tags: [Knowledge]
      summary: List the active version's chunks
      description: Requires `knowledge.view`. Returns deterministic chunk-index order for the source's active `chunked` version. Original text is the verbatim extracted slice; normalized text is a search-only representation. The opaque cursor is bound to that immutable version. A source outside every tenant visible to the caller returns 404, and a version that is not chunked returns 409.
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/PageLimit'
      responses:
        '200':
          description: Cursor-paginated chunks for the active immutable source version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeChunkListEnvelope'
        '404':
          description: The source is not visible in any authorized tenant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: The active source version is not chunked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        default:
          $ref: '#/components/responses/Error'
components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: ahs_session
    csrfHeader:
      type: apiKey
      in: header
      name: X-CSRF-Token
    publishableKey:
      type: apiKey
      in: header
      name: X-AHS-Publishable-Key
    apiKey:
      type: apiKey
      in: header
      name: X-AHS-API-Key
    conversationToken:
      type: apiKey
      in: header
      name: X-AHS-Conversation-Token
  parameters:
    CSRFToken:
      name: X-CSRF-Token
      in: header
      required: true
      description: Token paired with the active session for state-changing browser requests.
      schema:
        type: string
        minLength: 43
        maxLength: 43
    OrganizationID:
      name: organization_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    UserID:
      name: user_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    SourceID:
      name: source_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ConversationID:
      name: conversation_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ProjectIDPath:
      name: project_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    EnvironmentIDPath:
      name: environment_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    PublishableKeyID:
      name: key_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ProjectIDQuery:
      name: project_id
      in: query
      required: true
      schema:
        type: string
        format: uuid
    EnvironmentIDQuery:
      name: environment_id
      in: query
      required: true
      schema:
        type: string
        format: uuid
    Cursor:
      name: cursor
      in: query
      required: false
      description: Opaque cursor returned by the previous page.
      schema:
        type: string
        minLength: 1
    PageLimit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    ConversationPageLimit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    ConversationCursor:
      name: cursor
      in: query
      required: false
      description: Opaque cursor returned by the previous conversation page and bound to its project/environment scope.
      schema:
        type: string
        minLength: 1
        maxLength: 512
  requestBodies:
    EmailRequest:
      required: true
      content:
        application/json:
          schema:
            type: object
            additionalProperties: false
            required: [email]
            properties:
              email:
                type: string
                format: email
                maxLength: 254
    TokenRequest:
      required: true
      content:
        application/json:
          schema:
            type: object
            additionalProperties: false
            required: [token]
            properties:
              token:
                type: string
                minLength: 43
                maxLength: 43
  responses:
    Accepted:
      description: The request was accepted without disclosing whether an account exists.
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestID'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AcceptedEnvelope'
    Error:
      description: Safe API error.
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestID'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  headers:
    RequestID:
      description: Correlation identifier for support and tracing.
      schema:
        type: string
        pattern: '^[A-Za-z0-9._-]{1,128}$'
  schemas:
    Role:
      type: string
      enum:
        [
          owner,
          admin,
          developer,
          knowledge_manager,
          support_manager,
          support_agent,
          analyst,
          billing
        ]
    InvitationRole:
      type: string
      enum:
        [
          admin,
          developer,
          knowledge_manager,
          support_manager,
          support_agent,
          analyst,
          billing
        ]
    EmailOTPRequest:
      type: object
      additionalProperties: false
      required: [email, locale]
      properties:
        email:
          type: string
          format: email
          maxLength: 254
        locale:
          type: string
          enum: [ar, en]
    EmailOTPVerifyRequest:
      type: object
      additionalProperties: false
      required: [challenge_token, code]
      properties:
        challenge_token:
          type: string
          minLength: 43
          maxLength: 43
        code:
          type: string
          pattern: '^[0-9]{6}$'
    EmailOTPProfileRequest:
      type: object
      additionalProperties: false
      required: [completion_token, first_name, last_name]
      properties:
        completion_token:
          type: string
          minLength: 43
          maxLength: 43
        first_name:
          type: string
          minLength: 1
          maxLength: 60
        last_name:
          type: string
          minLength: 1
          maxLength: 60
    CreateOrganizationRequest:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 120
    OrganizationDeletionChallengeRequest:
      type: object
      additionalProperties: false
      required: [locale]
      properties:
        locale:
          type: string
          enum: [ar, en]
    DeleteOrganizationRequest:
      type: object
      additionalProperties: false
      required: [challenge_token, code, confirmation_name]
      properties:
        challenge_token:
          type: string
          minLength: 43
          maxLength: 43
        code:
          type: string
          pattern: '^[0-9]{6}$'
        confirmation_name:
          type: string
          minLength: 2
          maxLength: 120
    AssistantSettingsInput:
      type: object
      additionalProperties: false
      required: [answer_length, tone, answer_format, clarification]
      properties:
        answer_length: { type: string, enum: [concise, balanced, detailed] }
        tone: { type: string, enum: [professional, friendly, formal] }
        answer_format: { type: string, enum: [auto, paragraphs, bullets] }
        clarification: { type: string, enum: [when_needed, never] }
    AssistantSettings:
      type: object
      additionalProperties: false
      required:
        [
          project_id,
          environment_id,
          answer_length,
          tone,
          answer_format,
          clarification,
          updated_at
        ]
      properties:
        project_id: { type: string, format: uuid }
        environment_id: { type: string, format: uuid }
        answer_length: { type: string, enum: [concise, balanced, detailed] }
        tone: { type: string, enum: [professional, friendly, formal] }
        answer_format: { type: string, enum: [auto, paragraphs, bullets] }
        clarification: { type: string, enum: [when_needed, never] }
        updated_at: { type: string, format: date-time }
    AssistantSettingsEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/AssistantSettings' }
    UpdateMemberRoleRequest:
      type: object
      additionalProperties: false
      required: [role]
      properties:
        role:
          $ref: '#/components/schemas/InvitationRole'
    CreateInvitationRequest:
      type: object
      additionalProperties: false
      required: [email, role, locale]
      properties:
        email:
          type: string
          format: email
        role:
          $ref: '#/components/schemas/InvitationRole'
        locale:
          type: string
          enum: [ar, en]
    CreateProjectRequest:
      type: object
      additionalProperties: false
      required: [name, default_language, support_use_case]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 120
        default_language:
          type: string
          enum: [ar, en]
        support_use_case:
          type: string
          enum: [customer_support, ecommerce, saas, other]
    User:
      type: object
      additionalProperties: false
      required: [id, email, display_name, locale, email_verified_at, created_at]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        display_name:
          type: string
        locale:
          type: string
          enum: [ar, en]
        email_verified_at:
          type: [string, 'null']
          format: date-time
        created_at:
          type: string
          format: date-time
    Organization:
      type: object
      additionalProperties: false
      required: [id, name, slug, role, created_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        role:
          $ref: '#/components/schemas/Role'
        created_at:
          type: string
          format: date-time
    Member:
      type: object
      additionalProperties: false
      required: [user_id, email, display_name, role, joined_at]
      properties:
        user_id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        display_name:
          type: string
        role:
          $ref: '#/components/schemas/Role'
        joined_at:
          type: string
          format: date-time
    Invitation:
      type: object
      additionalProperties: false
      required: [id, organization_id, email, role, expires_at, created_at]
      properties:
        id:
          type: string
          format: uuid
        organization_id:
          type: string
          format: uuid
        organization_name:
          type: string
        email:
          type: string
          format: email
        role:
          $ref: '#/components/schemas/InvitationRole'
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    ProjectEnvironment:
      type: object
      additionalProperties: false
      required: [id, kind]
      properties:
        id:
          type: string
          format: uuid
        kind:
          type: string
          enum: [test, production]
    Project:
      type: object
      additionalProperties: false
      required:
        [
          id,
          organization_id,
          name,
          slug,
          default_language,
          support_use_case,
          environments,
          created_at
        ]
      properties:
        id:
          type: string
          format: uuid
        organization_id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        default_language:
          type: string
          enum: [ar, en]
        support_use_case:
          type: string
          enum: [customer_support, ecommerce, saas, other]
        environments:
          type: array
          minItems: 2
          maxItems: 2
          items:
            $ref: '#/components/schemas/ProjectEnvironment'
        created_at:
          type: string
          format: date-time
    AuditEvent:
      type: object
      additionalProperties: false
      required:
        [
          id,
          action,
          object_type,
          object_id,
          actor_user_id,
          request_id,
          metadata,
          created_at
        ]
      properties:
        id:
          type: string
          format: uuid
        action:
          type: string
        object_type:
          type: string
        object_id:
          type: [string, 'null']
          format: uuid
        actor_user_id:
          type: [string, 'null']
          format: uuid
        request_id:
          type: string
        metadata:
          type: object
          additionalProperties: true
        created_at:
          type: string
          format: date-time
    KnowledgeIngestionStatus:
      type: string
      description: Extraction and deterministic chunking states for the active immutable version.
      enum: [stored, parsing, parsed, chunking, chunked, failed]
    KnowledgeIndexingStatus:
      type: string
      description: Provider-independent semantic indexing state. not_started also covers deployments where production embeddings are disabled.
      enum: [not_started, running, ready, failed]
    KnowledgeSourceVersion:
      type: object
      additionalProperties: false
      required:
        [
          id,
          version_number,
          original_filename,
          byte_size,
          content_type,
          checksum_sha256,
          status,
          page_or_section_count,
          detected_language,
          character_count,
          failure_reason,
          indexing_status,
          indexing_failure_reason,
          created_at
        ]
      properties:
        id:
          type: string
          format: uuid
        version_number:
          type: integer
          minimum: 1
        original_filename:
          type: string
          minLength: 1
          maxLength: 512
        byte_size:
          type: integer
          format: int64
          minimum: 1
        content_type:
          type: string
          enum:
            - application/pdf
            - application/vnd.openxmlformats-officedocument.wordprocessingml.document
            - text/plain; charset=utf-8
        checksum_sha256:
          type: string
          pattern: '^[a-f0-9]{64}$'
        status:
          $ref: '#/components/schemas/KnowledgeIngestionStatus'
        page_or_section_count:
          type: [integer, 'null']
          minimum: 1
          description: DOCX section count or one for plain text after successful parsing.
        detected_language:
          type: [string, 'null']
          enum: [ar, en, mixed, unknown, null]
        character_count:
          type: [integer, 'null']
          format: int64
          minimum: 0
        failure_reason:
          type: [string, 'null']
          enum:
            - page_limit_exceeded
            - output_limit_exceeded
            - archive_limit_exceeded
            - parse_timeout
            - malformed_document
            - no_extractable_text
            - object_read_failed
            - chunk_count_limit_exceeded
            - normalized_output_limit_exceeded
            - grapheme_cluster_limit_exceeded
            - chunking_failed
            - chunk_persistence_failed
            - null
        indexing_status:
          $ref: '#/components/schemas/KnowledgeIndexingStatus'
        indexing_failure_reason:
          type: [string, 'null']
          description: Stable safe reason; provider messages and customer source text are never exposed.
          enum:
            - embedding_timeout
            - embedding_provider_unavailable
            - embedding_provider_rejected
            - embedding_invalid_response
            - null
        created_at:
          type: string
          format: date-time
    KnowledgeSourceHistoryVersion:
      type: object
      additionalProperties: false
      required:
        [
          id,
          version_number,
          original_filename,
          byte_size,
          content_type,
          checksum_sha256,
          status,
          page_or_section_count,
          detected_language,
          character_count,
          failure_reason,
          indexing_status,
          indexing_failure_reason,
          created_at,
          is_active,
          is_pending
        ]
      properties:
        id: { type: string, format: uuid }
        version_number: { type: integer, minimum: 1 }
        original_filename: { type: string, minLength: 1, maxLength: 512 }
        byte_size: { type: integer, format: int64, minimum: 1 }
        content_type:
          type: string
          enum:
            [
              application/pdf,
              application/vnd.openxmlformats-officedocument.wordprocessingml.document,
              text/plain; charset=utf-8
            ]
        checksum_sha256: { type: string, pattern: '^[a-f0-9]{64}$' }
        status: { $ref: '#/components/schemas/KnowledgeIngestionStatus' }
        page_or_section_count: { type: [integer, 'null'], minimum: 1 }
        detected_language:
          { type: [string, 'null'], enum: [ar, en, mixed, unknown, null] }
        character_count: { type: [integer, 'null'], format: int64, minimum: 0 }
        failure_reason: { type: [string, 'null'], maxLength: 512 }
        indexing_status:
          { $ref: '#/components/schemas/KnowledgeIndexingStatus' }
        indexing_failure_reason: { type: [string, 'null'], maxLength: 512 }
        created_at: { type: string, format: date-time }
        is_active: { type: boolean }
        is_pending: { type: boolean }
    ActivateKnowledgeSourceVersionInput:
      type: object
      additionalProperties: false
      required: [version_id]
      properties:
        version_id:
          type: string
          format: uuid
    KnowledgeSource:
      type: object
      additionalProperties: false
      required:
        [
          id,
          project_id,
          environment_id,
          original_filename,
          active_version,
          created_at,
          updated_at
        ]
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        environment_id:
          type: string
          format: uuid
        original_filename:
          type: string
          minLength: 1
          maxLength: 512
        active_version:
          $ref: '#/components/schemas/KnowledgeSourceVersion'
        pending_version:
          description: A not-yet-active immutable replacement. It disappears after atomic activation and may expose a stable ingestion or indexing failure while the previous active version remains live.
          oneOf:
            - $ref: '#/components/schemas/KnowledgeSourceVersion'
            - type: 'null'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AcceptedEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [accepted]
          properties:
            accepted:
              type: boolean
              const: true
    EmailOTPChallengeEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [challenge_token]
          properties:
            challenge_token:
              type: string
              minLength: 43
              maxLength: 43
    EmailOTPVerifyEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [requires_profile]
          properties:
            requires_profile:
              type: boolean
            completion_token:
              type: string
              minLength: 43
              maxLength: 43
            user:
              $ref: '#/components/schemas/User'
    UserEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [user]
          properties:
            user:
              $ref: '#/components/schemas/User'
    SessionEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [user, expires_at]
          properties:
            user:
              $ref: '#/components/schemas/User'
            expires_at:
              type: string
              format: date-time
    OrganizationEnvelope:
      $ref: '#/components/schemas/SingleOrganizationData'
    OrganizationDeletionChallengeEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [challenge_token]
          properties:
            challenge_token:
              type: string
              minLength: 43
              maxLength: 43
    SingleOrganizationData:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/Organization'
    OrganizationListEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Organization'
    MemberEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/Member'
    MemberListEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Member'
    InvitationEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/Invitation'
    InvitationListEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Invitation'
    ProjectEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/Project'
    ProjectListEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Project'
    AuditEventListEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/AuditEvent'
    KnowledgeSourceEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/KnowledgeSource'
    KnowledgeSourceListEnvelope:
      type: object
      additionalProperties: false
      required: [data, pagination]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/KnowledgeSource'
        pagination:
          type: object
          additionalProperties: false
          required: [next_cursor]
          properties:
            next_cursor:
              type: [string, 'null']
    KnowledgeSourceVersionListEnvelope:
      type: object
      additionalProperties: false
      required: [data, pagination]
      properties:
        data:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/KnowledgeSourceHistoryVersion'
        pagination:
          type: object
          additionalProperties: false
          required: [next_cursor]
          properties:
            next_cursor:
              type: [string, 'null']
    KnowledgeSourcePreview:
      type: object
      additionalProperties: false
      required:
        [
          source_id,
          version_id,
          status,
          text,
          truncated,
          page_or_section_count,
          detected_language,
          character_count,
          failure_reason
        ]
      properties:
        source_id:
          type: string
          format: uuid
        version_id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/KnowledgeIngestionStatus'
        text:
          type: string
          maxLength: 16000
        truncated:
          type: boolean
        page_or_section_count:
          type: [integer, 'null']
          minimum: 1
        detected_language:
          type: [string, 'null']
          enum: [ar, en, mixed, unknown, null]
        character_count:
          type: [integer, 'null']
          format: int64
          minimum: 0
        failure_reason:
          type: [string, 'null']
          maxLength: 512
    KnowledgeSourcePreviewEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/KnowledgeSourcePreview'
    KnowledgeChunkLocator:
      type: object
      additionalProperties: false
      required: [section_index, heading_path, page]
      properties:
        section_index:
          type: integer
          minimum: 1
        heading_path:
          type: array
          maxItems: 6
          items:
            type: string
        page:
          type: [integer, 'null']
          minimum: 1
          description: One-based page number when a parser can establish it; null for the current DOCX and text parsers.
    KnowledgeChunk:
      type: object
      additionalProperties: false
      required:
        [
          id,
          chunk_index,
          original_text,
          normalized_search_text,
          detected_language,
          locator,
          checksum_sha256,
          chunker_version
        ]
      properties:
        id:
          type: string
          format: uuid
        chunk_index:
          type: integer
          minimum: 0
        original_text:
          type: string
          minLength: 1
          description: Verbatim contiguous slice of the extracted source text.
        normalized_search_text:
          type: string
          description: Search-only Arabic normalization; the original text remains unchanged.
        detected_language:
          type: string
          enum: [ar, en, mixed, unknown]
        locator:
          $ref: '#/components/schemas/KnowledgeChunkLocator'
        checksum_sha256:
          type: string
          pattern: '^[a-f0-9]{64}$'
          description: SHA-256 of the original chunk text's UTF-8 bytes.
        chunker_version:
          type: string
          minLength: 1
          maxLength: 64
    KnowledgeChunkListEnvelope:
      type: object
      additionalProperties: false
      required: [data, pagination]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/KnowledgeChunk'
        pagination:
          type: object
          additionalProperties: false
          required: [next_cursor]
          properties:
            next_cursor:
              type: [string, 'null']
    KnowledgeSearchRequest:
      type: object
      additionalProperties: false
      required: [project_id, environment_id, query]
      properties:
        project_id:
          type: string
          format: uuid
        environment_id:
          type: string
          format: uuid
        query:
          type: string
          minLength: 1
          maxLength: 8192
          description: UTF-8 customer question. The server also enforces an 8 KiB encoded-byte maximum.
        limit:
          type: integer
          minimum: 1
          maximum: 20
          default: 5
    KnowledgeSearchHit:
      type: object
      additionalProperties: false
      required:
        [
          chunk_id,
          source_id,
          source_version_id,
          source_filename,
          chunk_index,
          text,
          detected_language,
          locator,
          score
        ]
      properties:
        chunk_id:
          type: string
          format: uuid
        source_id:
          type: string
          format: uuid
        source_version_id:
          type: string
          format: uuid
        source_filename:
          type: string
          minLength: 1
          maxLength: 512
        chunk_index:
          type: integer
          minimum: 0
        text:
          type: string
          minLength: 1
          description: Verbatim chunk text for grounded context and later citation assembly.
        detected_language:
          type: string
          enum: [ar, en, mixed, unknown]
        locator:
          $ref: '#/components/schemas/KnowledgeChunkLocator'
        score:
          type: number
          format: double
          minimum: -1
          maximum: 1
          description: Exact cosine similarity. This is retrieval evidence, not an answer-confidence threshold.
    KnowledgeSearchEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          maxItems: 20
          items:
            $ref: '#/components/schemas/KnowledgeSearchHit'
    PlaygroundAnswerRequest:
      type: object
      additionalProperties: false
      required: [project_id, environment_id, question, language]
      properties:
        project_id:
          type: string
          format: uuid
        environment_id:
          type: string
          format: uuid
        question:
          type: string
          minLength: 1
          maxLength: 8192
          description: UTF-8 customer question. The server also enforces an 8 KiB encoded-byte maximum.
        language:
          type: string
          enum: [ar, en, mixed]
    CreatePublishableKeyRequest:
      type: object
      additionalProperties: false
      required: [name, allowed_origins]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 80
        allowed_origins:
          type: array
          minItems: 1
          maxItems: 10
          uniqueItems: true
          items:
            type: string
            format: uri
    CreateServerKeyRequest:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 80
        allowed_ip_addresses:
          type: array
          minItems: 0
          maxItems: 32
          uniqueItems: true
          default: []
          items:
            type: string
            format: ip
    PublishableKey:
      type: object
      additionalProperties: false
      required:
        [
          id,
          project_id,
          environment_id,
          name,
          key_prefix,
          allowed_origins,
          allowed_ip_addresses,
          key_type,
          https_required,
          created_at,
          last_used_at,
          revoked_at
        ]
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        environment_id:
          type: string
          format: uuid
        name:
          type: string
        key_prefix:
          type: string
          description: Non-secret display prefix; it cannot authenticate requests.
        allowed_origins:
          type: array
          items:
            type: string
            format: uri
        allowed_ip_addresses:
          type: array
          maxItems: 0
          items:
            type: string
            format: ip
        key_type:
          type: string
          const: publishable
        https_required:
          type: boolean
          const: true
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: [string, 'null']
          format: date-time
        revoked_at:
          type: [string, 'null']
          format: date-time
    CreatedPublishableKey:
      type: object
      additionalProperties: false
      required:
        [
          id,
          project_id,
          environment_id,
          name,
          key_prefix,
          allowed_origins,
          allowed_ip_addresses,
          key_type,
          https_required,
          created_at,
          last_used_at,
          revoked_at,
          secret
        ]
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        environment_id:
          type: string
          format: uuid
        name:
          type: string
        key_prefix:
          type: string
          description: Non-secret display prefix; it cannot authenticate requests.
        allowed_origins:
          type: array
          items:
            type: string
            format: uri
        allowed_ip_addresses:
          type: array
          maxItems: 0
          items:
            type: string
            format: ip
        key_type:
          type: string
          const: publishable
        https_required:
          type: boolean
          const: true
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: [string, 'null']
          format: date-time
        revoked_at:
          type: [string, 'null']
          format: date-time
        secret:
          type: string
          minLength: 100
          description: Complete publishable key shown exactly once. It is browser-visible and accepted only for its exact allowed origins and environment; it is not an administrative secret.
    PublishableKeyListEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublishableKey'
    CreatedPublishableKeyEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/CreatedPublishableKey'
    ServerKey:
      type: object
      additionalProperties: false
      required:
        [
          id,
          project_id,
          environment_id,
          name,
          key_prefix,
          allowed_origins,
          allowed_ip_addresses,
          key_type,
          https_required,
          created_at,
          last_used_at,
          revoked_at
        ]
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        environment_id: { type: string, format: uuid }
        name: { type: string }
        key_prefix:
          type: string
          description: Non-secret display prefix; it cannot authenticate requests.
        allowed_origins:
          type: array
          maxItems: 0
          items: { type: string, format: uri }
        allowed_ip_addresses:
          type: array
          maxItems: 32
          items: { type: string, format: ip }
        key_type: { type: string, const: server }
        https_required: { type: boolean, const: false }
        created_at: { type: string, format: date-time }
        last_used_at: { type: [string, 'null'], format: date-time }
        revoked_at: { type: [string, 'null'], format: date-time }
    CreatedServerKey:
      type: object
      additionalProperties: false
      required:
        [
          id,
          project_id,
          environment_id,
          name,
          key_prefix,
          allowed_origins,
          allowed_ip_addresses,
          key_type,
          https_required,
          created_at,
          last_used_at,
          revoked_at,
          secret
        ]
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        environment_id: { type: string, format: uuid }
        name: { type: string }
        key_prefix:
          type: string
          description: Non-secret display prefix; it cannot authenticate requests.
        allowed_origins:
          type: array
          maxItems: 0
          items: { type: string, format: uri }
        allowed_ip_addresses:
          type: array
          maxItems: 32
          items: { type: string, format: ip }
        key_type: { type: string, const: server }
        https_required: { type: boolean, const: false }
        created_at: { type: string, format: date-time }
        last_used_at: { type: [string, 'null'], format: date-time }
        revoked_at: { type: [string, 'null'], format: date-time }
        secret:
          type: string
          minLength: 100
          description: Complete server secret shown exactly once. Keep it only in trusted server-side secret storage; never expose it to browsers or mobile clients.
    ServerKeyListEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/ServerKey' }
    CreatedServerKeyEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/CreatedServerKey' }
    ServerAnswerRequest:
      type: object
      additionalProperties: false
      required: [question, language]
      properties:
        question:
          type: string
          minLength: 1
          maxLength: 8192
        language:
          type: string
          enum: [ar, en, mixed]
    ServerAnswerEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [status, answer, citations, handoff_suggested]
          properties:
            status: { type: string, enum: [grounded, uncertain] }
            answer: { type: string }
            citations:
              type: array
              items:
                type: object
                additionalProperties: false
                required: [citation_id, source_filename]
                properties:
                  citation_id: { type: string }
                  source_filename: { type: string }
            handoff_suggested: { type: boolean }
    WidgetBootstrapEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [project_name, default_language, maximum_question_bytes]
          properties:
            project_name:
              type: string
            default_language:
              type: string
              enum: [ar, en]
            maximum_question_bytes:
              type: integer
              const: 8192
    WidgetAnswerRequest:
      type: object
      additionalProperties: false
      required: [question, language]
      properties:
        question:
          type: string
          minLength: 1
          maxLength: 8192
        language:
          type: string
          enum: [ar, en, mixed]
          description: Required response language; mixed currently uses the Arabic uncertainty fallback.
    WidgetConversationMessage:
      type: object
      additionalProperties: false
      required: [sequence_number, actor_type, body, language, created_at]
      properties:
        sequence_number:
          type: integer
          format: int64
          minimum: 1
        actor_type:
          type: string
          const: agent
        body:
          type: string
          minLength: 1
          maxLength: 32768
        language:
          type: string
          enum: [ar, en, mixed]
        created_at:
          type: string
          format: date-time
    WidgetConversationMessagePage:
      type: object
      additionalProperties: false
      required: [status, last_sequence, messages]
      properties:
        status:
          $ref: '#/components/schemas/ConversationStatus'
        last_sequence:
          type: integer
          format: int64
          minimum: 0
        messages:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/WidgetConversationMessage'
    WidgetConversationMessagePageEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/WidgetConversationMessagePage'
    WidgetPublicCitation:
      type: object
      additionalProperties: false
      required: [citation_id, source_filename]
      properties:
        citation_id:
          type: string
        source_filename:
          type: string
    WidgetConversationHistoryMessage:
      type: object
      additionalProperties: false
      required: [sequence_number, actor_type, body, language, created_at]
      properties:
        sequence_number:
          type: integer
          format: int64
          minimum: 1
        actor_type:
          type: string
          enum: [customer, assistant, agent]
        body:
          type: string
          minLength: 1
          maxLength: 32768
        language:
          type: string
          enum: [ar, en, mixed]
        created_at:
          type: string
          format: date-time
        citations:
          type: array
          maxItems: 64
          items:
            $ref: '#/components/schemas/WidgetPublicCitation'
        feedback:
          type: string
          enum: [positive, negative]
    WidgetConversationHistory:
      type: object
      additionalProperties: false
      required: [status, last_sequence, has_more, messages]
      properties:
        status:
          $ref: '#/components/schemas/ConversationStatus'
        last_sequence:
          type: integer
          format: int64
          minimum: 0
        has_more:
          type: boolean
        messages:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/WidgetConversationHistoryMessage'
    WidgetConversationHistoryEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/WidgetConversationHistory'
    WidgetFeedbackInput:
      type: object
      additionalProperties: false
      required: [message_sequence, rating]
      properties:
        message_sequence:
          type: integer
          format: int64
          minimum: 1
        rating:
          type: string
          enum: [positive, negative]
    WidgetFeedbackResult:
      type: object
      additionalProperties: false
      required: [message_sequence, rating, updated_at]
      properties:
        message_sequence:
          type: integer
          format: int64
          minimum: 1
        rating:
          type: string
          enum: [positive, negative]
        updated_at:
          type: string
          format: date-time
    WidgetFeedbackEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/WidgetFeedbackResult'
    WidgetCustomerMessageRequest:
      type: object
      additionalProperties: false
      required: [body, language]
      properties:
        body:
          type: string
          minLength: 1
          maxLength: 32768
        language:
          type: string
          enum: [ar, en, mixed]
    WidgetCustomerMessage:
      type: object
      additionalProperties: false
      required: [sequence_number, actor_type, body, language, created_at]
      properties:
        sequence_number:
          type: integer
          format: int64
          minimum: 3
        actor_type:
          type: string
          const: customer
        body:
          type: string
          minLength: 1
          maxLength: 32768
        language:
          type: string
          enum: [ar, en, mixed]
        created_at:
          type: string
          format: date-time
    WidgetCustomerMessageResult:
      type: object
      additionalProperties: false
      required: [status, last_sequence, message]
      properties:
        status:
          type: string
          enum: [handoff_requested, queued, human_active]
        last_sequence:
          type: integer
          format: int64
          minimum: 3
        message:
          $ref: '#/components/schemas/WidgetCustomerMessage'
    WidgetCustomerMessageResultEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/WidgetCustomerMessageResult'
    BillingEntitlement:
      type: object
      additionalProperties: false
      required: [key, enabled, limit_value]
      properties:
        key: { type: string }
        enabled: { type: boolean }
        limit_value: { type: [integer, 'null'], format: int64, minimum: 0 }
    BillingPlan:
      type: object
      additionalProperties: false
      required: [id, entitlements, checkout_available]
      properties:
        id: { type: string, enum: [starter, growth, business] }
        entitlements:
          type: array
          items: { $ref: '#/components/schemas/BillingEntitlement' }
        checkout_available: { type: boolean }
    BillingOverviewEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required:
            [
              subscription,
              entitlements,
              usage,
              plans,
              can_manage,
              portal_available
            ]
          properties:
            subscription:
              type: object
              additionalProperties: false
              required:
                [
                  plan_id,
                  status,
                  current_period_start,
                  current_period_end,
                  trial_ends_at,
                  cancel_at_period_end,
                  updated_at
                ]
              properties:
                plan_id: { type: string, enum: [starter, growth, business] }
                status:
                  {
                    type: string,
                    enum:
                      [
                        active,
                        trialing,
                        past_due,
                        canceled,
                        incomplete,
                        incomplete_expired,
                        unpaid,
                        paused
                      ]
                  }
                provider: { type: string, enum: [stripe] }
                current_period_start:
                  { type: [string, 'null'], format: date-time }
                current_period_end:
                  { type: [string, 'null'], format: date-time }
                trial_ends_at: { type: [string, 'null'], format: date-time }
                cancel_at_period_end: { type: boolean }
                updated_at: { type: string, format: date-time }
            entitlements:
              type: array
              items: { $ref: '#/components/schemas/BillingEntitlement' }
            usage:
              type: array
              items:
                type: object
                additionalProperties: false
                required: [key, quantity, from, to]
                properties:
                  key: { type: string }
                  quantity: { type: integer, format: int64, minimum: 0 }
                  from: { type: string, format: date-time }
                  to: { type: string, format: date-time }
            plans:
              type: array
              items: { $ref: '#/components/schemas/BillingPlan' }
            can_manage: { type: boolean }
            portal_available: { type: boolean }
    BillingRedirectEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [url]
          properties:
            url: { type: string, format: uri }
    PlaygroundCitation:
      type: object
      additionalProperties: false
      required:
        [
          citation_id,
          chunk_id,
          source_id,
          source_version_id,
          source_filename,
          chunk_index,
          language,
          locator
        ]
      properties:
        citation_id:
          type: string
          pattern: '^C[1-9][0-9]*$'
        chunk_id:
          type: string
          format: uuid
        source_id:
          type: string
          format: uuid
        source_version_id:
          type: string
          format: uuid
        source_filename:
          type: string
          minLength: 1
          maxLength: 512
        chunk_index:
          type: integer
          minimum: 0
        language:
          type: string
          enum: [ar, en, mixed, unknown]
        locator:
          $ref: '#/components/schemas/KnowledgeChunkLocator'
    PlaygroundAnswer:
      type: object
      additionalProperties: false
      required: [status, answer, citations, handoff_suggested]
      properties:
        status:
          type: string
          enum: [grounded, uncertain]
        answer:
          type: string
          minLength: 1
          maxLength: 32768
          description: UTF-8 grounded or localized uncertainty text. The server also enforces a 32 KiB encoded-byte maximum.
        citations:
          type: array
          maxItems: 8
          items:
            $ref: '#/components/schemas/PlaygroundCitation'
        handoff_suggested:
          type: boolean
          description: True when AHS recommends human support because the answer is uncertain.
    PlaygroundAnswerEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/PlaygroundAnswer'
    ConversationStatus:
      type: string
      enum:
        [
          ai_active,
          handoff_requested,
          queued,
          human_active,
          waiting_customer,
          resolved,
          closed
        ]
    ConversationChannel:
      type: string
      enum: [playground, web, api]
    Conversation:
      type: object
      additionalProperties: false
      required:
        [
          id,
          project_id,
          environment_id,
          status,
          channel,
          language,
          created_at,
          updated_at,
          last_message_at,
          assignment
        ]
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        environment_id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/ConversationStatus'
        channel:
          $ref: '#/components/schemas/ConversationChannel'
        language:
          type: string
          enum: [ar, en, mixed]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        last_message_at:
          type: string
          format: date-time
        assignment:
          oneOf:
            - $ref: '#/components/schemas/ConversationAssignment'
            - type: 'null'
    ConversationAssignment:
      type: object
      additionalProperties: false
      required: [user_id, display_name, email, assigned_at]
      properties:
        user_id:
          type: string
          format: uuid
        display_name:
          type: string
          minLength: 2
          maxLength: 120
        email:
          type: string
          format: email
          maxLength: 254
        assigned_at:
          type: string
          format: date-time
    ConversationAssignmentRequest:
      type: object
      additionalProperties: false
      required: [assignee_user_id]
      properties:
        assignee_user_id:
          type: string
          format: uuid
    ConversationAssignmentEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/ConversationAssignment'
    ConversationHandoffAction:
      type: string
      enum: [request, queue, accept, wait_for_customer, resume, resolve, close]
    ConversationHandoffRequest:
      type: object
      additionalProperties: false
      required: [action]
      properties:
        action:
          $ref: '#/components/schemas/ConversationHandoffAction'
    ConversationHandoffState:
      type: object
      additionalProperties: false
      required: [status, updated_at]
      properties:
        status:
          $ref: '#/components/schemas/ConversationStatus'
        updated_at:
          type: string
          format: date-time
    ConversationHandoffEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/ConversationHandoffState'
    CreateConversationAgentMessageRequest:
      type: object
      additionalProperties: false
      required: [body, language]
      properties:
        body:
          type: string
          minLength: 1
          maxLength: 32768
        language:
          type: string
          enum: [ar, en, mixed]
    ConversationMessageEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/ConversationMessage'
    ConversationSource:
      type: object
      additionalProperties: false
      required:
        [
          citation_index,
          source_id,
          source_version_id,
          chunk_id,
          source_filename,
          chunk_index,
          language,
          locator
        ]
      properties:
        citation_index:
          type: integer
          minimum: 0
          maximum: 63
        source_id:
          type: string
          format: uuid
        source_version_id:
          type: string
          format: uuid
        chunk_id:
          type: string
          format: uuid
        source_filename:
          type: string
          minLength: 1
          maxLength: 512
        chunk_index:
          type: integer
          minimum: 0
          maximum: 4095
        language:
          type: string
          enum: [ar, en, mixed, unknown]
        locator:
          $ref: '#/components/schemas/KnowledgeChunkLocator'
    ConversationMessage:
      type: object
      additionalProperties: false
      required:
        [
          id,
          sequence_number,
          actor_type,
          body,
          language,
          answer_status,
          handoff_suggested,
          created_at,
          sources
        ]
      properties:
        id:
          type: string
          format: uuid
        sequence_number:
          type: integer
          format: int64
          minimum: 1
        actor_type:
          type: string
          enum: [customer, assistant, agent, system]
        body:
          type: string
          minLength: 1
          maxLength: 32768
          description: Immutable UTF-8 message body; the server enforces a 32 KiB encoded-byte maximum.
        language:
          type: string
          enum: [ar, en, mixed]
        answer_status:
          type: [string, 'null']
          enum: [grounded, uncertain, null]
        handoff_suggested:
          type: boolean
        created_at:
          type: string
          format: date-time
        sources:
          type: array
          maxItems: 64
          items:
            $ref: '#/components/schemas/ConversationSource'
    ConversationDetail:
      type: object
      additionalProperties: false
      required:
        [
          id,
          project_id,
          environment_id,
          status,
          channel,
          language,
          created_at,
          updated_at,
          last_message_at,
          assignment,
          messages
        ]
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        environment_id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/ConversationStatus'
        channel:
          $ref: '#/components/schemas/ConversationChannel'
        language:
          type: string
          enum: [ar, en, mixed]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        last_message_at:
          type: string
          format: date-time
        assignment:
          oneOf:
            - $ref: '#/components/schemas/ConversationAssignment'
            - type: 'null'
        messages:
          type: array
          items:
            $ref: '#/components/schemas/ConversationMessage'
    ConversationListEnvelope:
      type: object
      additionalProperties: false
      required: [data, pagination]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Conversation'
        pagination:
          type: object
          additionalProperties: false
          required: [next_cursor]
          properties:
            next_cursor:
              type: [string, 'null']
    ConversationDetailEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/ConversationDetail'
    Liveness:
      type: object
      additionalProperties: false
      required: [status]
      properties:
        status:
          type: string
          const: ok
    ComponentStatus:
      type: object
      additionalProperties: false
      required: [name, status]
      properties:
        name:
          type: string
          enum: [email, nats, object_store, postgres, redis]
        status:
          type: string
          enum: [ready, unavailable]
    FoundationStatus:
      type: object
      additionalProperties: false
      required: [service, version, status, time, components]
      properties:
        service:
          type: string
          example: ahs-api
        version:
          type: string
          example: dev
        status:
          type: string
          enum: [ready, unavailable]
        time:
          type: string
          format: date-time
        components:
          type: array
          items:
            $ref: '#/components/schemas/ComponentStatus'
    ErrorEnvelope:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [code, message, request_id]
          properties:
            code:
              type: string
              example: dependency_unavailable
            message:
              type: string
              example: The service is temporarily unavailable.
            request_id:
              type: string
              example: req_0123456789abcdef
