openapi: 3.0.0
info:
  title: Nurama API
  version: 0.3.3
  description: |-
    REST API for the Nurama platform. All endpoints are served under `/v1`.

    Authenticate with a user JWT (`Authorization: Bearer <token>`) against `api.nurama.com`, or with a bot API key
    (`nrm_bot_…`) against `bot.nurama.com`. Only success and route-specific error responses are documented for each
    operation; `400` (validation), `401` (unauthenticated) and `403` (forbidden) can be returned by any authenticated
    endpoint and share the `Error` schema.

    Real-time events are delivered over Socket.IO and documented separately as an AsyncAPI document.
servers:
  - url: https://api.nurama.com/v1
    description: Production
  - url: https://bot.nurama.com/v1
    description: Production (bot API keys)
paths:
  /assets/{assetId}/access-activity:
    post:
      summary: Record a play event from an authenticated player
      description: |
        Reports a play event from an authenticated player. Events are
        de-duplicated per UTC day per session, so resuming from pause or
        replaying a region within one session counts once. `download` and
        `embed_resolved` are NOT accepted on this route — those are recorded
        by the API automatically to keep the counts authoritative.
      tags:
        - AccessActivity
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                eventType:
                  type: string
                  enum:
                    - play_started
                    - play_completed
                visibility:
                  type: string
                  enum:
                    - creator
                    - reviewer
                  description: |
                    Audience side the play happened on. `public` is reserved
                    for the unauthenticated /public-download/{token} route.
              required:
                - eventType
                - visibility
              additionalProperties: false
      responses:
        '204':
          description: Event recorded.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - bearerAuth: []
      x-request-source: joi
    get:
      summary: Read aggregated access activity for one asset
      description: |
        Returns totals per eventType, a top-N breakdown for one dimension,
        and a zero-filled daily series for the requested eventType. Gated
        by `canGetAccessActivity` — granted to the same roles that hold
        `canGetPublicLinks`.
      tags:
        - AccessActivity
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: range
          in: query
          required: false
          schema:
            type: string
            default: 30d
            enum:
              - 7d
              - 30d
              - 90d
        - name: groupBy
          in: query
          required: false
          schema:
            type: string
            default: visibility
            enum:
              - visibility
              - referrerHost
              - country
              - userAgentClass
              - day
        - name: eventType
          in: query
          required: false
          schema:
            type: string
            enum:
              - play_started
              - play_completed
              - download
              - embed_resolved
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
      responses:
        '200':
          description: Aggregated stats.
          content:
            application/json:
              schema:
                type: object
                properties:
                  range:
                    type: string
                    example: 30d
                  from:
                    type: string
                    example: '2026-04-13'
                  to:
                    type: string
                    example: '2026-05-13'
                  totals:
                    type: array
                    items:
                      type: object
                      properties:
                        eventType:
                          type: string
                        count:
                          type: integer
                  breakdown:
                    type: array
                    items:
                      type: object
                      properties:
                        value:
                          type: string
                          description: |
                            The dimension value. Empty string represents
                            "Unknown" (see `label` for display).
                        label:
                          type: string
                          example: Unknown
                        count:
                          type: integer
                  series:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          example: '2026-05-13'
                        eventType:
                          type: string
                        count:
                          type: integer
                  groupBy:
                    type: string
                  eventType:
                    type: string
                    nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - bearerAuth: []
      x-request-source: joi
  /public-download/{token}/access-activity:
    post:
      summary: Record a play event from an embedded player (unauthenticated)
      description: |
        Reports a play event from an embedded (unauthenticated) player. The
        token is the 10-character public-link token and is the only
        credential — no bearer token is used. Events on this route are
        always recorded with `visibility: 'public'`; the body accepts no
        `visibility` field. Only recorded while the link is `active`: a
        missing or disabled link is rejected with 404 (deliberately
        indistinguishable), an expired one with 410.
      tags:
        - AccessActivity
        - PublicAssetLinks
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            pattern: ^[a-zA-Z0-9]{10}$
            example: Ab3dEf9HiJ
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                eventType:
                  type: string
                  enum:
                    - play_started
                    - play_completed
              required:
                - eventType
              additionalProperties: false
      responses:
        '204':
          description: Event recorded.
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Token not found or link not active (`publicAssetLinkNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '410':
          description: Link expired (`publicAssetLinkExpired`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
  /projects/{projectId}/access-activity/top-assets:
    get:
      summary: Top-N assets in a project by access count
      description: |
        Returns `(assetId, count)` pairs ordered by total count of the
        requested `eventType` in the range. Only ids and counts are
        returned; fetch names and thumbnails through the asset endpoints.
        Requires `canGetAccessActivity`.
      tags:
        - AccessActivity
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: range
          in: query
          required: false
          schema:
            type: string
            default: 30d
            enum:
              - 7d
              - 30d
              - 90d
        - name: eventType
          in: query
          required: false
          schema:
            type: string
            default: play_started
            enum:
              - play_started
              - play_completed
              - download
              - embed_resolved
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
      responses:
        '200':
          description: Top-N assets.
          content:
            application/json:
              schema:
                type: object
                properties:
                  range:
                    type: string
                  from:
                    type: string
                  to:
                    type: string
                  eventType:
                    type: string
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        assetId:
                          type: string
                          format: uuid
                        count:
                          type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - bearerAuth: []
      x-request-source: joi
  /ai/polish:
    post:
      summary: Polish a draft message in the requested tone.
      description: |
        Rewrites the user's draft message according to the requested tone while
        preserving meaning, intent, and any technical terms. In the Nurama web
        app this backs the chat composer's "Polish with Nu" button.

        Returns the rewritten text plus the credits actually billed and the
        workspace's balance after deduction.

        Caps:
          - `text` is capped at 4000 characters (~1000 input tokens).
          - Output is capped at ~600 tokens server-side.

        If the workspace has an `aiCustomPreprompt` configured it is prepended
        as additional system context (workspace-specific guidance to the model).
      tags:
        - AI
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workspaceId:
                  type: string
                  format: uuid
                  description: Workspace whose credits will be billed and whose AI settings gate the call.
                text:
                  type: string
                  minLength: 1
                  maxLength: 4000
                  description: The user's draft message to be rewritten.
                toneId:
                  type: string
                  enum:
                    - professional
                    - casual
                    - concise
                    - friendly
                    - formal
                    - cleanup
                  description: One of the IDs returned by `GET /ai/tones`.
                projectId:
                  type: string
                  format: uuid
                  description: Optional project context, recorded on the usage event for reporting.
              required:
                - workspaceId
                - text
                - toneId
              additionalProperties: false
      responses:
        '200':
          description: Draft polished successfully. Credits have been deducted and a usage event recorded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AiPolishResponse'
              examples:
                ok:
                  summary: Successful polish
                  value:
                    polishedText: Could you please send me the file when you have a moment? Thank you.
                    billedCredits: 3
                    balanceAfter: 9997
                    eventId: 0193e4a1-b2c3-7000-8000-00000000abcd
        '400':
          description: |
            Validation error, or a gate / tool error. Possible `type` values:
              - `validationError` — the request body failed validation
              - `aiToneNotFound` — `toneId` not in the catalogue
              - `aiDraftEmpty` — `text` empty or whitespace-only
              - `aiDraftTooLong` — `text` exceeds the 4000-character cap
              - `subscriptionNotActive` — the workspace has no active subscription
              - `creditBalanceInsufficient` — workspace balance is below the
                worst-case estimate (`errorData: { creditsRequired, currentBalance }`)
              - `creditOrderExceedsUserDailySpendLimit` — the caller has hit
                their daily AI spend cap (`errorData: { creditsRequired, userDailySpend, userDailySpendBudget }`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: |
            Forbidden. Possible `type` values:
              - `forbidden` — the caller does not hold `canAiPolishInput` on the workspace.
              - `capabilityNotAvailable` — the workspace's plan lacks the `ai`
                capability or `aiAddOnEnabled` is off (`errorData: { capability: 'ai', workspaceId }`).
              - `aiFeatureNotEnabled` — effective AI settings (project
                overrides workspace) have `allowAiFeatures` or `aiPolishEnabled`
                off, or the caller is a reviewer and `aiPolishAllowReviewer` is off.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '502':
          description: |
            The AI provider call failed. `type` is `aiProviderFailure` or
            `aiProviderEmptyResponse`. No credits are deducted; no event is recorded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters: []
      x-request-source: joi
  /ai/task-generation:
    post:
      summary: Convert a chat message into one or more board-task drafts.
      description: |
        Generates a list of `{ subject, description }` task drafts from a
        chat message. In the Nurama web app this backs the "Generate with Nu"
        option in a message's create-task menu; the returned `tasks` array
        seeds the task creator's draft list.

        **Compound capability requirement.** Unlike Polish, this endpoint
        requires BOTH the `boards` capability (checked against the project)
        and the `ai` capability (checked against the workspace); both
        `workspaceId` and `projectId` must belong to an active subscription.
        Either capability missing returns `capabilityNotAvailable`.

        **Context window.** `contextMessages` is the preceding chat
        thread (oldest first). The server caps at 10 messages and each
        entry's `content` at 8000 characters; supply the messages immediately
        before the focal one so the model can resolve pronouns and references.

        **Permission.** Requires `canAiGenerateTasks` (held by all
        workspace/project admins and by the creator, reviewer and
        reviewerAdmin roles).

        **Pre-prompt.** If the workspace or project has
        `aiTaskGenerationPreprompt` configured, it is appended to the
        system prompt as an override block (project value wins per-key).
        Separate from `aiCustomPreprompt` so admins can tune task style
        without affecting Polish or Chat output.

        Caps:
          - `messageText` is capped at 8000 characters.
          - `contextMessages` is capped at 10 entries; each entry's
            `content` is capped at 8000 characters and `authorName` at 100.
          - At most 10 tasks are returned.
      tags:
        - AI
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workspaceId:
                  type: string
                  format: uuid
                  description: Workspace whose credits will be billed and whose AI settings gate the call.
                projectId:
                  type: string
                  format: uuid
                  description: |
                    Project the generated tasks will belong to. Required: the
                    Boards capability is checked against this project, and the
                    project's AI settings take precedence over the workspace's.
                messageText:
                  type: string
                  minLength: 1
                  maxLength: 8000
                  description: The focal chat message text to convert into tasks.
                messageId:
                  type: string
                  format: uuid
                  description: |
                    Optional source chat message id. Not used today; reserved
                    for future audit linking between the usage event and the
                    originating message.
                contextMessages:
                  type: array
                  items:
                    type: object
                    properties:
                      authorName:
                        type: string
                        maxLength: 100
                        nullable: true
                        description: Display name of the message author. Optional but improves prompt clarity.
                      content:
                        type: string
                        minLength: 1
                        maxLength: 8000
                    required:
                      - content
                    additionalProperties: false
                  maxItems: 10
                  description: |
                    Preceding chat messages (oldest first) for
                    conversational lead-in. The focal message is NOT
                    duplicated here.
              required:
                - workspaceId
                - projectId
                - messageText
              additionalProperties: false
      responses:
        '200':
          description: Tasks generated successfully. Credits have been deducted and a usage event recorded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AiTaskGenerationResponse'
              examples:
                ok:
                  summary: Two tasks generated from the focal message
                  value:
                    tasks:
                      - subject: Draft launch checklist
                        description: List every step needed before go-live.
                      - subject: Notify stakeholders
                        description: Email the launch plan to leadership.
                    billedCredits: 4
                    balanceAfter: 9996
                    eventId: 0193e4a1-b2c3-7000-8000-00000000abcd
        '400':
          description: |
            Validation error, or a gate / tool error. Possible `type` values:
              - `validationError` — too many `contextMessages`, oversized item, etc.
              - `aiTaskGenerationInputEmpty` — `messageText` empty/whitespace
              - `aiTaskGenerationInputTooLong` — `messageText` exceeds the 8000-char cap
              - `subscriptionNotActive` — the workspace or project has no active subscription
              - `creditBalanceInsufficient` / `creditOrderExceedsUserDailySpendLimit`
                — balance below the worst-case estimate, or the caller's daily cap is hit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: |
            Forbidden. Possible `type` values:
              - `forbidden` — the caller does not hold `canAiGenerateTasks` on the workspace.
              - `capabilityNotAvailable` — the project's workspace lacks the
                `boards` capability, or the workspace lacks the `ai` capability
                (`errorData.capability` says which).
              - `aiFeatureNotEnabled` — effective AI settings (project
                overrides workspace) have `allowAiFeatures` or
                `aiTaskGenerationEnabled` off (or `aiTaskGenerationAllowReviewer` for reviewers).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '502':
          description: |
            Upstream failure. Possible `type` values:
              - `aiProviderFailure` — the AI provider call failed.
              - `aiProviderEmptyResponse` — the provider returned no usable content.
              - `aiTaskGenerationInvalidOutput` — the provider returned content
                that could not be parsed into tasks, or contained zero usable
                tasks. Safe to retry.
            No credits are deducted on a provider-side failure; no event is recorded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters: []
      x-request-source: joi
  /ai/compose:
    post:
      summary: One turn of a Compose-with-Nu drafting conversation.
      description: |
        Nu helps the user draft a message for a specific chat. The client sends
        the drafting conversation so far (`messages`, oldest first); the server
        reads the target chat for context and tone and returns Nu's commentary
        (`text`) plus, when it has one, a `proposal` — the message Nu suggests
        posting. Nothing is posted; the user decides whether the proposal
        reaches their composer.

        Requires `canAiComposeMessage` on the workspace, an active
        subscription, the `ai` capability, `aiComposeEnabled` (and
        `aiComposeAllowReviewer` for reviewers) and sufficient balance — see
        the AI overview. The caller must also be able to read `chatId`, so
        you can only compose into a chat you can already read.

        Caps: 1–40 turns; each turn's `text` / `proposal` ≤ 4000 characters.
      tags:
        - AI
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workspaceId:
                  type: string
                  format: uuid
                  description: Workspace whose credits will be billed and whose AI settings gate the call.
                chatId:
                  type: string
                  format: uuid
                  description: >-
                    The chat being composed into. Read server-side for context; the client never sends transcript
                    content.
                projectId:
                  type: string
                  format: uuid
                  description: Optional project context for settings resolution and analytics.
                messages:
                  type: array
                  items:
                    type: object
                    properties:
                      role:
                        type: string
                        enum:
                          - user
                          - assistant
                      text:
                        type: string
                        maxLength: 4000
                        description: The turn's text. May be empty.
                      proposal:
                        type: string
                        maxLength: 4000
                        nullable: true
                        description: >-
                          For assistant turns, the proposal Nu previously offered — replayed so Nu can see what it
                          already suggested.
                    required:
                      - role
                      - text
                    additionalProperties: false
                  minItems: 1
                  maxItems: 40
                  description: The drafting conversation so far, oldest first.
              required:
                - workspaceId
                - chatId
                - messages
              additionalProperties: false
      responses:
        '200':
          description: Turn completed. Credits have been deducted and a usage event recorded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AiComposeResponse'
              examples:
                ok:
                  value:
                    text: 'Here''s a short version you could post:'
                    proposal: Heads up — the review has moved to Thursday. Same time, same link.
                    billedCredits: 4
                    balanceAfter: 9993
                    eventId: 0193e4a1-b2c3-7000-8000-00000000abcd
        '400':
          description: |
            Validation error, or `subscriptionNotActive`,
            `creditBalanceInsufficient`, `creditOrderExceedsUserDailySpendLimit`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: |
            `forbidden` (no `canAiComposeMessage`, or the caller cannot read
            `chatId`), `capabilityNotAvailable`, or `aiFeatureNotEnabled`
            (`aiComposeEnabled` / `aiComposeAllowReviewer` off).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '502':
          description: Upstream provider failure (`aiProviderFailure`). No credits are deducted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters: []
      x-request-source: joi
  /ai/feedback:
    post:
      summary: Rate one assistant reply (Nu Feedback).
      description: |
        Records a thumbs-up / thumbs-down on a single assistant reply, together
        with a **snapshot of the interaction that produced it**, for the Nurama
        team to review.

        **What gets stored.** The reply being rated, up to ten preceding turns
        of the conversation, and — when the client supplies them — the page
        context and pinned Context-column items that were in play at the time.
        Each captured message is truncated at 4,000 characters. Tell the user
        before submitting that context from the interaction will be shared
        with the Nurama team for review.

        The interaction is **snapshotted, not referenced**: the record captures
        what the assistant actually said at the time, and remains useful if
        the chat is later deleted or the topic rewritten.

        **Authorisation.** The caller must be able to read `chatId` — the same
        check the chat endpoints apply. A user can only rate a reply in a chat
        they can already read, so this endpoint cannot be used to capture
        someone else's conversation by guessing message ids. Failure is
        `403 forbidden`.

        This endpoint is **not** subject to the subscription, AI-feature or
        credit-balance checks that gate AI use: feedback can be submitted even
        when a workspace's AI add-on has been switched off.

        A snapshot that cannot be built does not fail the request — the rating is
        still recorded, with `context.snapshotError: true`. Losing a thumbs-down
        to a transcript read failure would be the wrong trade.
      tags:
        - AI
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                rating:
                  type: string
                  enum:
                    - positive
                    - negative
                  description: The face the user picked.
                surface:
                  type: string
                  enum:
                    - chat
                    - assist
                  description: |
                    Which AI surface produced the reply — `chat` for Nu Chat /
                    Nu Support, `assist` for Nu @-mentioned inside a regular
                    chat. The two run different prompts and tool sets and are
                    tuned separately.
                chatId:
                  type: string
                  format: uuid
                  description: |
                    Chat the rated reply lives in. Required: it is what the
                    access check runs against and what the snapshot is built
                    from.
                messageId:
                  type: string
                  format: uuid
                  description: The assistant reply being rated.
                notes:
                  type: string
                  maxLength: 2000
                  description: |
                    The user's own words on what worked or didn't. Optional — a
                    rating with no explanation is still a signal.
                workspaceId:
                  type: string
                  format: uuid
                projectId:
                  type: string
                  format: uuid
                pageContext:
                  type: object
                  additionalProperties: true
                  description: |
                    The page context the assistant was given for the rated
                    interaction. Loosely typed on purpose — the shape may
                    change over time, and a submission is never rejected over a
                    shape drift.
                contextItems:
                  type: array
                  items:
                    type: object
                    additionalProperties: true
                  description: Pinned Context-column items, capped at 50.
              required:
                - rating
                - surface
                - chatId
              additionalProperties: false
      responses:
        '201':
          description: Feedback recorded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
              examples:
                ok:
                  value:
                    id: 0192f3a4-5b6c-7d8e-9f01-23456789abcf
        '400':
          description: Validation error — unknown `rating`/`surface`, or notes over 2,000 characters.
        '401':
          description: Unauthorized — missing or invalid bearer token.
        '403':
          description: |
            `forbidden` — the caller cannot read `chatId`. Same gate the chat
            routes apply, so this also covers a chat that does not exist.
      parameters: []
      x-request-source: joi
  /ai/tones:
    get:
      summary: List polish tones available to the chat composer.
      description: |
        Returns the polish tone catalogue so clients can offer tone choices
        without hard-coding them. The list is currently fixed; workspace-scoped
        custom tones are planned.
      tags:
        - AI
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Tone catalogue.
          content:
            application/json:
              schema:
                type: object
                properties:
                  tones:
                    type: array
                    items:
                      $ref: '#/components/schemas/AiTone'
              examples:
                ok:
                  summary: Default tone catalogue
                  value:
                    tones:
                      - id: professional
                        label: Professional
                      - id: casual
                        label: Casual
                      - id: concise
                        label: Concise
                      - id: friendly
                        label: Friendly
                      - id: formal
                        label: Formal
                      - id: cleanup
                        label: Cleanup
        '401':
          description: Unauthorized — missing or invalid bearer token.
  /ai/balance:
    get:
      summary: Get the workspace's current Nurama Credit balance.
      description: |
        Returns the workspace's spendable Nurama Credit balance.

        Requires `canGetCreditBalance` on the workspace (workspace owner /
        admin, project owner / admin) and an active subscription; no
        per-feature gate. The balance is also returned as `balanceAfter` on
        every billable AI call, so clients can keep a balance display fresh
        without a separate fetch after every call.

        `balance` is the spendable total: `planBalance` (monthly plan grant,
        does not carry over) + `purchasedBalance` (top-ups, accumulates).
        `hasPriorTopUp` reports whether the workspace has ever made a manual
        credit purchase — the precondition for enabling auto top-up.

        Also available at `GET /credits/balance` (same response).
      tags:
        - AI
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: query
          required: true
          schema:
            type: string
            format: uuid
          description: Workspace whose Nurama Credit balance to retrieve.
      responses:
        '200':
          description: Current credit balance (integer credits; 0 if the workspace has never been granted credits).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AiBalanceResponse'
              examples:
                ok:
                  summary: Current balance
                  value:
                    balance: 9997
                    planBalance: 5000
                    purchasedBalance: 4997
                    hasPriorTopUp: true
        '400':
          description: '`subscriptionNotActive` — the workspace has no active subscription.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized — missing or invalid bearer token.
        '403':
          description: |
            Forbidden — the caller does not hold `canGetCreditBalance` on the workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
  /ai/usage:
    get:
      summary: Aggregated Nurama Credit usage report for a workspace.
      description: |
        Credit spend on AI features (and convos) in a time window, totalled
        and broken down by user and by integration point. Defaults to a
        rolling 30-day window ending now when no dates are passed. Filters
        apply uniformly: `userId` narrows the per-integration breakdown to
        that user's calls, and `integrationPoint` narrows the per-user
        breakdown to that feature.

        Requires `canGetCreditUsageReport` on the workspace (granted to the
        same roles as `canGetCreditBalance`) and an active subscription.

        Also available at `GET /credits/usage` (same response).
      tags:
        - AI
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: startDate
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: ISO 8601 date-time. Defaults to 30 days before `endDate`.
        - name: endDate
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: ISO 8601 date-time; must not be before `startDate`. Defaults to now.
        - name: userId
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: integrationPoint
          in: query
          required: false
          schema:
            type: string
            enum:
              - polish
              - chat
              - taskGeneration
              - imageRevision
              - convo
      responses:
        '200':
          description: Usage report for the window.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AiUsageReportResponse'
        '400':
          description: Validation error (bad dates, unknown `integrationPoint`), or `subscriptionNotActive`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden — the caller does not hold `canGetCreditUsageReport` on the workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
  /ai/chat/topics:
    get:
      summary: List the caller's AI chat topics for a scope.
      description: |
        Returns topics owned by the calling user, scoped to one
        workspace / project, or to "social" (personal) chats. Topics are
        ordered by `lastMessageAt` desc with nulls last.
      tags:
        - AI
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: scopeType
          in: query
          required: true
          schema:
            type: string
            enum:
              - workspace
              - project
              - social
        - name: scopeId
          in: query
          required: false
          schema: {}
          description: Required when `scopeType` is `workspace` or `project`; must be omitted for `social`.
        - name: archived
          in: query
          required: false
          schema:
            type: boolean
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Topic list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AiChatTopicListResponse'
        '400':
          description: Validation error, or `subscriptionNotActive`.
        '401':
          description: Unauthorized.
        '403':
          description: |
            Forbidden — the scope-access check failed (no `canGetProject` /
            `canGetWorkspace` on `scopeId`; `forbidden`), the workspace lacks
            the `ai` capability (`capabilityNotAvailable`), or `aiChatEnabled`
            resolved false (`aiFeatureNotEnabled`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
    post:
      summary: Create a new (empty) AI chat topic.
      description: |
        Creates a new, empty topic. Send the first message through
        `POST /v1/chats/{topic.chatId}/new-message`; the assistant's reply
        arrives asynchronously as an `aiChatMessageCreate` notification (see
        the AI overview). The balance check runs when a message is sent, so
        no credits are charged by this endpoint.

        The first user/assistant exchange triggers an auto-naming pass; the
        resulting title arrives via the `aiChatTopicUpdate` notification.

        Requires access to the scope (`canGetProject` / `canGetWorkspace` on
        `scopeId`; no check for `social`), an active subscription, the `ai`
        capability and `aiChatEnabled`.
      tags:
        - AI
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workspaceId:
                  type: string
                  format: uuid
                  description: Workspace whose credits will be billed for the conversation.
                scopeType:
                  type: string
                  enum:
                    - workspace
                    - project
                    - social
                scopeId:
                  description: Required when scopeType is `workspace` or `project`; must be omitted for `social`.
                title:
                  type: string
                  maxLength: 120
                  description: Optional user-supplied title; otherwise the topic is auto-named after the first exchange.
              required:
                - workspaceId
                - scopeType
              additionalProperties: false
      responses:
        '201':
          description: Topic created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AiChatCreateTopicResponse'
        '400':
          description: Validation error (bad scope shape, `scopeId` present on a social topic, etc.), or `subscriptionNotActive`.
        '401':
          description: Unauthorized.
        '403':
          description: |
            Forbidden — scope access (`forbidden`), `ai` capability
            (`capabilityNotAvailable`), or `aiChatEnabled` (`aiFeatureNotEnabled`) gate failed.
      parameters: []
      x-request-source: joi
  /ai/chat/topics/{topicId}:
    get:
      summary: Get a single AI chat topic.
      tags:
        - AI
      security:
        - bearerAuth: []
      parameters:
        - name: topicId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: workspaceId
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The topic.
          content:
            application/json:
              schema:
                type: object
                properties:
                  topic:
                    $ref: '#/components/schemas/AiChatTopic'
        '400':
          description: '`subscriptionNotActive` — `workspaceId` has no active subscription.'
        '401':
          description: Unauthorized.
        '403':
          description: |
            Forbidden — the caller is not the topic's creator (`forbidden`),
            the workspace lacks the `ai` capability (`capabilityNotAvailable`),
            or `aiChatEnabled` is off (`aiFeatureNotEnabled`).
        '404':
          description: |
            Topic not found. `type` is `aiChatTopicNotFound`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
    patch:
      summary: Rename, archive, or set the context pins of an AI chat topic.
      description: |
        At least one of `title`, `archived`, or `contextItems` must be
        provided. Only the topic's creator may update it; any other caller
        receives `forbidden`.

        ## Context pins (`contextItems`)

        The items the user has pinned to Nu's Context column for this topic —
        assets, folders, people, submissions, and public collections that the
        conversation is "about". Persisted here rather than sent per message,
        so the column survives a page reload and a new session the way the
        conversation does.

        Sent and stored as **identity only** (`type` + `id`); the server
        normalises away any other field. Names, thumbnails, and content are
        never stored, and neither is any access decision.

        A pin is a **bookmark, not a grant**. Every entry is re-resolved from
        scratch on every turn, under the caller's live permissions:

          - an item deleted since it was pinned drops out of that turn;
          - an item the caller has since lost access to drops out;
          - visibility tiers are re-derived per turn — the stored pin
            carries none.

        Dropped entries are silently omitted from the model's context rather
        than reported, so the assistant never learns that something it cannot
        see exists. The stored list is left intact, so regaining access
        restores the pin without re-pinning.

        Sending `contextItems` replaces the topic's pins wholesale. An empty
        array clears them. Only project-scoped topics resolve pins at all;
        workspace- and social-scoped topics have no project to resolve
        against and ignore them.
      tags:
        - AI
      security:
        - bearerAuth: []
      parameters:
        - name: topicId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workspaceId:
                  type: string
                  format: uuid
                title:
                  type: string
                  minLength: 1
                  maxLength: 120
                archived:
                  type: boolean
                contextItems:
                  type: array
                  items:
                    type: object
                    properties:
                      type:
                        type: string
                        enum:
                          - asset
                          - folder
                          - user
                          - submission
                          - public
                      id:
                        type: string
                        maxLength: 200
                        description: |
                          UUID for every type except `public`, which is keyed by its share token.
                    required:
                      - type
                      - id
                    additionalProperties: false
                  maxItems: 20
                  description: >
                    Replaces the topic's Context-column pins wholesale. An empty array clears them. Identity only — see
                    the endpoint description for how these are re-resolved per turn.
              required:
                - workspaceId
              additionalProperties: false
              x-dependencies:
                - or:
                    - title
                    - archived
                    - contextItems
      responses:
        '200':
          description: Updated topic.
          content:
            application/json:
              schema:
                type: object
                properties:
                  topic:
                    $ref: '#/components/schemas/AiChatTopic'
        '400':
          description: Validation error (none of `title` / `archived` / `contextItems` supplied), or `subscriptionNotActive`.
        '401':
          description: Unauthorized.
        '403':
          description: Forbidden — not the topic's creator, `capabilityNotAvailable`, or `aiFeatureNotEnabled`.
        '404':
          description: Topic not found (`aiChatTopicNotFound`).
      x-request-source: joi
    delete:
      summary: Delete an AI chat topic.
      description: |
        Soft-deletes the topic: the topic and every message in its chat move
        to `status: pendingDelete`, and an `aiChatTopicUpdate` notification
        with a `delete` change is emitted on the caller's user channel. Only
        the topic's creator may delete it.
      tags:
        - AI
      security:
        - bearerAuth: []
      parameters:
        - name: topicId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: workspaceId
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: 'Topic deleted. Returns the topic with `status: pendingDelete`.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  topic:
                    $ref: '#/components/schemas/AiChatTopic'
        '400':
          description: '`subscriptionNotActive` — `workspaceId` has no active subscription.'
        '401':
          description: Unauthorized.
        '403':
          description: Forbidden — not the topic's creator, `capabilityNotAvailable`, or `aiFeatureNotEnabled`.
        '404':
          description: Topic not found (`aiChatTopicNotFound`).
      x-request-source: joi
  /ai/revisions:
    post:
      tags:
        - AI
      summary: Generate an AI image revision.
      description: |
        Generate a variation of an existing image (or a captured video
        frame) guided by a free-text prompt. The result is staged as a
        temporary Scratch upload — call `POST /scratch/{id}/promote` to turn
        the chosen revision into a real asset on the destination project
        (there is no `/ai/revisions/promote` route).

        `source` must contain exactly one of `url` (public media URL),
        `scratchId` (an upload created via `POST /ai/revisions/source-upload`
        — the preferred path for video frames; it must belong to the caller
        and workspace and still be active), or `dataUrl` (deprecated legacy
        path for video frames).

        Requires `canAiGenerateImageRevision` on the workspace, an active
        subscription, the `ai` capability, the `aiImageRevisionEnabled`
        workspace/project setting (`aiImageRevisionAllowReviewer` for
        reviewers) and a worst-case credit pre-check (a flat estimate).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workspaceId:
                  type: string
                  format: uuid
                projectId:
                  type: string
                  format: uuid
                assetId:
                  type: string
                  format: uuid
                  description: |
                    Optional, audit-only: the asset whose public media URL
                    was supplied as `source.url`. Recorded for lineage.
                source:
                  type: object
                  properties:
                    url:
                      type: string
                      format: uri
                      description: Public media URL of the source image.
                    dataUrl:
                      type: string
                      pattern: ^data:image\/[a-zA-Z0-9.+-]+;base64,
                      description: Base64 image data URL. Legacy path — prefer `scratchId`.
                      deprecated: true
                    scratchId:
                      type: string
                      format: uuid
                      description: Scratch upload id returned by `POST /ai/revisions/source-upload`.
                  additionalProperties: false
                  x-dependencies:
                    - xor:
                        - url
                        - dataUrl
                        - scratchId
                  description: |
                    Exactly one of `url`, `scratchId` or `dataUrl` (sending
                    two is a validation error).
                prompt:
                  type: string
                  minLength: 1
                  maxLength: 1000
                  description: Plain-language description of the desired edit.
                references:
                  type: array
                  items:
                    oneOf:
                      - type: string
                        format: uri
                      - type: string
                        pattern: ^data:image\/[a-zA-Z0-9.+-]+;base64,
                  maxItems: 8
                  description: |
                    Optional reference images (public URLs or base64 image
                    data URLs). Used for multi-image conditioning when
                    iterating on a prior revision.
                mask:
                  type: object
                  properties:
                    dataUrl:
                      type: string
                      pattern: ^data:image\/png;base64,
                      description: '`data:image/png;base64,…` — PNG only (JPEG has no alpha channel).'
                  required:
                    - dataUrl
                  additionalProperties: false
                  description: |
                    Optional alpha-channel mask. PNG data URL matching
                    the source image's dimensions. Transparent pixels
                    mark the region to edit; opaque pixels are preserved.
                    Applied as a true mask, not as another `references[]`
                    entry. When supplied, the prompt is automatically
                    augmented with a region directive.
                jobId:
                  type: string
                  format: uuid
                  description: Groups all revisions made in one editing session, for cleanup and analytics.
                sourceAspect:
                  type: number
                  minimum: 0.1
                  x-exclusiveMinimum: true
                  maximum: 10
                  description: |
                    Source image aspect ratio (width / height). Drives the
                    generation size bucket (square / landscape / portrait) and
                    the post-generate pad-to-aspect step.
              required:
                - workspaceId
                - source
                - prompt
              additionalProperties: false
      responses:
        '200':
          description: Generation succeeded; returns the scratch id + public URL.
          content:
            application/json:
              schema:
                type: object
                properties:
                  revisionId:
                    type: string
                    format: uuid
                  revisionUrl:
                    type: string
                    nullable: true
                  model:
                    type: string
                  quality:
                    type: string
                  billedCredits:
                    type: integer
                  balanceAfter:
                    type: integer
                    nullable: true
                  eventId:
                    type: string
                    format: uuid
                    nullable: true
        '400':
          description: |
            Validation error. `aiRevisionMaskInvalid` when `mask.dataUrl`
            isn't a PNG data URL. `aiRevisionPromptEmpty` /
            `aiRevisionPromptTooLong` on prompt-shape errors.
            `aiRevisionTooManyReferences` when `references[]` exceeds
            the maximum (8). `aiRevisionSourceMissing` when the source is
            absent or `scratchId` does not resolve to an active
            revision-source upload. Also `subscriptionNotActive`,
            `creditBalanceInsufficient`, `creditOrderExceedsUserDailySpendLimit`.
        '401':
          description: Unauthenticated.
        '403':
          description: |
            Forbidden — `forbidden` (no `canAiGenerateImageRevision`, or the
            `scratchId` belongs to another user / workspace),
            `capabilityNotAvailable`, or `aiFeatureNotEnabled` when
            `aiImageRevisionEnabled` is off.
        '502':
          description: Upstream provider failure (`aiProviderFailure`).
      parameters: []
      x-request-source: joi
  /ai/revisions/source-upload:
    post:
      tags:
        - AI
      summary: Mint a Scratch upload bundle for an image-revision source frame.
      description: |
        Returns signed multipart upload URLs the client uses to upload a
        source image (a captured video frame — always JPEG) directly to
        storage BEFORE calling `POST /ai/revisions` with `source.scratchId`.
        The bytes never pass through the API. After uploading the parts,
        complete the upload via `POST /v1/scratch/{scratchId}/complete-upload`.

        Same checks as `POST /ai/revisions` minus the balance check (staging
        is free): `canAiGenerateImageRevision`, an active subscription, the
        `ai` capability and `aiImageRevisionEnabled`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workspaceId:
                  type: string
                  format: uuid
                projectId:
                  type: string
                  format: uuid
                assetId:
                  type: string
                  format: uuid
                  description: Optional, audit-only lineage — the asset the frame was captured from.
                sizeInMB:
                  type: number
                  minimum: 0
                  x-exclusiveMinimum: true
                  maximum: 25
                  description: >-
                    Size of the JPEG in MB; decides how many upload parts are issued. Maximum 25 MB (the provider's
                    per-image cap).
              required:
                - workspaceId
                - sizeInMB
              additionalProperties: false
      responses:
        '200':
          description: Upload bundle. Multipart-upload the bytes to `urls`, then complete the upload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  scratchId:
                    type: string
                    format: uuid
                    description: Pass as `source.scratchId` to `POST /ai/revisions`.
                  key:
                    type: string
                    description: Storage key of the uploaded object.
                  uploadId:
                    type: string
                  urls:
                    type: array
                    items:
                      type: string
                      format: uri
                    description: Per-part signed PUT URLs.
                  expires:
                    type: string
                    format: date-time
                    description: When the staged upload expires.
        '400':
          description: Validation error, or `subscriptionNotActive`.
        '401':
          description: Unauthenticated.
        '403':
          description: '`forbidden`, `capabilityNotAvailable`, or `aiFeatureNotEnabled` (`aiImageRevisionEnabled` off).'
      parameters: []
      x-request-source: joi
  /assets/{assetId}:
    get:
      summary: Get asset information with optional chat data
      description: Retrieve information for a specific asset by ID. Optionally include chat data by specifying chatVisibility.
      tags:
        - Assets
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset to retrieve.
        - name: chatVisibility
          in: query
          required: false
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Visibility level of chat to include in response. If not specified, no chat data will be populated.
        - name: chatMessageSort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for chat messages.
        - name: chatReplySort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for chat replies.
        - name: chatMessageLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Maximum number of chat messages to return.
        - name: chatReplyLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Maximum number of chat replies to return per message.
      responses:
        '200':
          description: Successfully retrieved asset information.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Asset'
                  - type: object
                    properties:
                      chats:
                        type: object
                        description: Chat data for the asset (only present if chatVisibility is specified)
                        properties:
                          creator:
                            allOf:
                              - $ref: '#/components/schemas/Chat'
                            description: Creator chat data (only present if chatVisibility includes 'creator')
                          reviewer:
                            allOf:
                              - $ref: '#/components/schemas/Chat'
                            description: Reviewer chat data (only present if chatVisibility includes 'reviewer')
        '400':
          description: Bad request - Invalid asset ID or query parameters.
        '401':
          description: Unauthorized - Authentication required.
        '403':
          description: Forbidden - User does not have permission to access the asset or specified chat visibility level.
        '404':
          description: Asset not found.
      security:
        - bearerAuth: []
      x-request-source: joi
    put:
      summary: Update asset
      description: |
        Modify an existing asset (name, meta, sizeInBytes, status). Requires `canUpdateAsset`.
        `status` cannot be set to `pendingDelete` here — deletion goes through `DELETE /assets/{assetId}`.
      tags:
        - Assets
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset to update.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 100
                  description: New display name. HTML is sanitised.
                  example: Asset Name
                meta:
                  type: object
                sizeInBytes:
                  type: number
                  minimum: 0
                status:
                  type: string
                  enum:
                    - active
                    - pendingUpload
                    - pendingPostProcessing
                    - postProcessingError
                    - inactive
                  description: Any asset status except `pendingDelete`.
              additionalProperties: false
              example:
                name: New Asset Name
                meta:
                  key1: value1
                  key2: value2
      responses:
        '200':
          description: Successfully updated asset information.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Asset'
        '400':
          description: Bad request.
        '401':
          description: Unauthorized.
        '403':
          description: Forbidden - the user does not have permission to update the asset.
        '404':
          description: Asset not found.
      security:
        - bearerAuth: []
      x-request-source: joi
    delete:
      summary: Mark asset for deletion
      description: Mark an asset as deleted.
      tags:
        - Assets
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset to mark for deletion.
      responses:
        '200':
          description: Successfully marked asset for deletion. Returns the asset with `status` set to `pendingDelete`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Asset'
        '400':
          description: Bad request.
        '401':
          description: Unauthorized.
        '403':
          description: Forbidden - the user does not have permission to delete the asset.
        '404':
          description: Asset not found.
      security:
        - bearerAuth: []
      x-request-source: joi
  /assets/{assetId}/references:
    get:
      summary: List every location an asset is referenced
      description: >
        One asset can be referenced from many places at once — its own folder, the reviewer tree, any submission it was
        added to, any public release it was shared in. They all point at the same asset, which is why renaming it
        retitles it everywhere, and why deleting the last reference in its primary file system removes the rest.


        PRIMARY references are the asset's home file system; SECONDARY are everything else, grouped by collection. Only
        collections with at least one reference appear in `counts`.
      tags:
        - Assets
      security:
        - bearerAuth: []
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the asset
      responses:
        '200':
          description: Every location the asset is referenced
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssetReferences'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      x-request-source: joi
  /assets/{assetId}/file/{fileId}:
    get:
      summary: Get file from asset
      description: |
        Retrieve a specific file from an asset by asset ID and file ID. Requires `canGetAsset`.
        Private fields (`bucketName`, `tags`, `postProcessingTasks`) are stripped from the response.
      tags:
        - Assets
      responses:
        '200':
          description: Successfully retrieved file information.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/File'
        '400':
          $ref: '#/components/responses/FileNotFound'
        '401':
          description: Unauthorized.
        '403':
          description: Forbidden - the user does not have permission to get the file.
      security:
        - bearerAuth: []
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset containing the file.
        - name: fileId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the file to retrieve.
      x-request-source: joi
  /assets/{assetId}/function-type/{functionType}:
    get:
      summary: Get files by function type
      description: |
        Retrieve all files from an asset filtered by function type. Requires `canGetAsset`.
        Private fields (`bucketName`, `tags`, `postProcessingTasks`) are stripped from each file.
      tags:
        - Assets
      responses:
        '200':
          description: Successfully retrieved files of the specified function type.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/File'
        '400':
          $ref: '#/components/responses/FileNotFound'
        '401':
          description: Unauthorized.
        '403':
          description: Forbidden - the user does not have permission to get the files.
      security:
        - bearerAuth: []
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset to retrieve files from.
        - name: functionType
          in: path
          required: true
          schema:
            type: string
            enum:
              - original
              - thumbnail
              - stream
              - media
              - customThumbnailOriginal
              - customThumbnail
              - documentText
          description: The function type of files to filter by.
      x-request-source: joi
  /assets/repair:
    post:
      summary: Bulk regenerate signed upload links for assets in status pendingUpload.
      description: |
        Marks every existing file on each asset `pendingDelete` and mints a fresh multipart
        upload URL set so the client can re-upload the original. Requires `canRepairAssets`.
        Per-asset failures are reported inline (`status: 'error'`) rather than failing the request.
      tags:
        - Assets
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                assetIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 100
              additionalProperties: false
      responses:
        '200':
          description: One result entry per requested asset.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepairAssetAndSignedLinks'
        '400':
          description: Bad request.
        '403':
          description: Forbidden - the user does not have permission to repair one or more assets.
      security:
        - bearerAuth: []
      parameters: []
      x-request-source: joi
  /assets/complete-upload:
    post:
      summary: Complete multipart upload
      description: |
        Complete a multipart upload for an asset file.

        This endpoint finalizes the multipart upload by combining all uploaded parts,
        registers the original file on the asset (`assetId` is required) and triggers
        asset completion and media post-processing.

        **Two-step workflow:**
        1. Upload the file parts to the signed URLs returned when the asset was created
        2. Call this endpoint with `assetId` to complete the upload and trigger processing

        Post-processing failures after the upload has been committed do not fail the
        request — they are reported via the optional `warning` field.
      tags:
        - Assets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                key:
                  type: string
                  description: Storage key of the upload, as returned with the signed upload URLs.
                uploadId:
                  type: string
                  description: Multipart upload ID returned with the signed upload URLs.
                parts:
                  type: array
                  items:
                    type: object
                    properties:
                      ETag:
                        type: string
                      PartNumber:
                        type: integer
                    required:
                      - ETag
                      - PartNumber
                    additionalProperties: false
                  description: Array of uploaded parts with ETags
                assetId:
                  type: string
                  format: uuid
                  description: |
                    ID of the asset the upload belongs to. Once the upload is committed the original
                    file is registered on the asset and asset completion / media processing is triggered.
              required:
                - key
                - uploadId
                - assetId
              additionalProperties: false
      responses:
        '200':
          description: >-
            Upload committed to storage. `warning` is present when the asset could not be found or post-processing could
            not be triggered.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - completed
                  warning:
                    type: string
                    description: Present only when a non-fatal follow-up step failed.
                    example: Post-processing failed
        '400':
          $ref: '#/components/responses/BadRequest'
      security:
        - bearerAuth: []
      parameters: []
      x-request-source: joi
  /assets/page/{assetId}:
    get:
      summary: Get the page an asset would appear on in paginated results.
      description: |
        Returns the 1-based page number the asset would appear on when listing assets of the
        same owner resource and media type with the given sort and page size. Requires `canGetAsset`.
        The asset must be `active`.
      tags:
        - Assets
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset to find the page number for
        - name: visibility
          in: query
          required: false
          schema:
            type: string
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              name:
                type: number
                enum:
                  - 1
                  - -1
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 20
            default: 20
          description: Page size used to compute the page number.
        - name: inFolder
          in: query
          required: false
          schema:
            type: boolean
          description: When true, the page is computed within the asset's folder rather than the whole owner resource.
      responses:
        '200':
          description: Successfully retrieved asset page data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  page:
                    type: integer
                    example: 3
        '400':
          $ref: '#/components/responses/AssetNotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - bearerAuth: []
      x-request-source: joi
  /assets/{assetId}/tag:
    put:
      summary: Add tag to asset
      description: Add a tag to an asset.
      tags:
        - Assets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
                  description: ID of the tag to add to the asset
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Tag added to asset successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Asset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/AssetNotFound'
      security:
        - bearerAuth: []
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset to tag.
      x-request-source: joi
  /assets/{assetId}/untag:
    put:
      summary: Remove tag from asset
      description: Remove a tag from an asset.
      tags:
        - Assets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
                  description: ID of the tag to remove from the asset
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Tag removed from asset successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Asset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/AssetNotFound'
      security:
        - bearerAuth: []
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset to untag.
      x-request-source: joi
  /assets/download:
    post:
      summary: Download assets
      description: |
        Generate download links for the specified assets/files. Requires `canDownloadAssets`.
        Side effect: a `download` event is recorded in each asset's access activity
        with the `visibility` given in the request body (defaults to `creator`).
        Recording the activity never affects the download response.

        Note: the request body is currently accepted without validation; the limits
        below reflect the intended contract.
      tags:
        - Assets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                assetIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 100
                visibility:
                  type: string
                  enum:
                    - creator
                    - reviewer
                  description: |
                    Audience side the download was initiated from; recorded as the
                    `visibility` of the access-activity event.
                    Defaults to `creator` when omitted.
              required:
                - assetIds
      responses:
        '200':
          description: Successfully generated download links.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DownloadSignedUrlData'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - bearerAuth: []
      parameters:
        - name: assetIds
          in: path
          required: true
          schema:
            type: array
            items:
              type: string
              format: uuid
            maxItems: 100
      x-request-source: joi
  /assets/{assetId}/custom-thumbnail/upload-url:
    post:
      summary: Mint a signed multipart upload URL for a custom thumbnail
      description: |
        Returns a multipart upload URL set for a user-supplied custom thumbnail
        image. Once the upload is finalized via
        `POST /assets/{assetId}/custom-thumbnail/complete-upload`, background
        processing generates the sized `customThumbnail` files and registers
        them on the asset.

        Side effect: any prior active `customThumbnailOriginal` / `customThumbnail`
        files on the asset are set to `inactive` before the URL is issued so the
        new upload replaces them cleanly. If the upload is abandoned, the asset's
        auto-generated `thumbnail` files remain and should be displayed instead.

        Custom thumbnails are only supported for `video` and `audio` assets.
      tags:
        - Assets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                fileName:
                  type: string
                  minLength: 1
                  maxLength: 255
                  description: Original file name; only its extension is used for the stored file.
                  example: cover.jpg
                mimeType:
                  type: string
                  enum:
                    - image/jpeg
                    - image/png
                    - image/gif
                    - image/tiff
                    - image/bmp
                    - image/webp
                    - image/svg+xml
                    - image/heif
                    - image/vnd.microsoft.icon
                    - image/aces
                    - image/heic
                  description: |
                    MIME type of the upload. Must correspond to one of the platform's
                    supported image types (see `supportedFileTypes.image` in `GET /config`).
                  example: image/jpeg
                sizeInMB:
                  type: number
                  minimum: 0
                  x-exclusiveMinimum: true
                  maximum: 10
                  description: Approximate file size in megabytes; must be positive, capped at 10.
                  example: 1.2
              required:
                - fileName
                - mimeType
                - sizeInMB
              additionalProperties: false
      responses:
        '200':
          description: Multipart upload URL set returned.
          content:
            application/json:
              schema:
                type: object
                properties:
                  fileName:
                    type: string
                    description: File name the upload is stored under (`<assetId>.<ext>`).
                  assetId:
                    type: string
                    format: uuid
                  uploadId:
                    type: string
                    description: Multipart upload ID; pass it back to `/complete-upload`.
                  key:
                    type: string
                    description: Storage key the upload writes to; pass it back to `/complete-upload`.
                  urls:
                    type: array
                    description: Signed PUT URLs, one per part.
                    items:
                      type: string
                  mimeType:
                    type: string
                  expires:
                    type: integer
                    description: Unix timestamp when the signed URLs expire.
                  status:
                    type: string
                    enum:
                      - success
        '400':
          description: Invalid mediaType (only video/audio supported), invalid mimeType, or invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Forbidden — caller lacks `canUpdateAsset` on this asset.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the video or audio asset to attach a custom thumbnail to.
      x-request-source: joi
  /assets/{assetId}/custom-thumbnail/complete-upload:
    post:
      summary: Finalize the multipart upload of a custom thumbnail
      description: |
        Commits the multipart upload of a custom thumbnail. Background processing
        then generates the sized `customThumbnail` files and registers them on the
        asset. Unlike `/v1/assets/complete-upload`, this does not register an
        original file on the asset and does not trigger media transcoding.
      tags:
        - Assets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                key:
                  type: string
                  description: Storage key of the in-progress multipart upload, as returned from `/upload-url`.
                uploadId:
                  type: string
                  description: Multipart upload ID returned from `/upload-url`.
                parts:
                  type: array
                  items:
                    type: object
                    properties:
                      ETag:
                        type: string
                      PartNumber:
                        type: integer
                        minimum: 1
                    required:
                      - ETag
                      - PartNumber
                    additionalProperties: false
                  minItems: 1
                  description: ETags collected from each part PUT, in order.
              required:
                - key
                - uploadId
                - parts
              additionalProperties: false
      responses:
        '200':
          description: Upload finalized.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - completed
                  key:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Forbidden — caller lacks `canUpdateAsset` on this asset.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      x-request-source: joi
  /assets/{assetId}/custom-thumbnail:
    delete:
      summary: Remove the custom thumbnail from an asset
      description: |
        Soft-deletes every active `customThumbnailOriginal` and `customThumbnail`
        file on the asset by setting their status to `inactive`, decrementing the
        asset's `sizeInBytes` and storage quota by the bytes that were active.
        Clients should fall back to the auto-generated `thumbnail` files.
        Broadcasts an `assetFileUpdate` WebSocket event so connected clients can
        re-render without a refetch.
      tags:
        - Assets
      responses:
        '200':
          description: Custom thumbnail removed; returns the updated asset.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Asset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Forbidden — caller lacks `canUpdateAsset` on this asset.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      x-request-source: joi
  /assets/{assetId}/document-url:
    get:
      summary: Get a signed inline-view URL for a document asset
      description: |
        Returns a short-lived signed URL for rendering a `document` asset inline in the
        viewer. Serves the processed `media` derivative (an optimised PDF with permission
        flags stripped), not the original — downloads of the original go through
        `POST /assets/download`. Requires `canGetAsset`.

        The asset must be `active`, have `mediaType: 'document'`, and have an active
        `media` file (i.e. post-processing finished successfully); otherwise a 400 is
        returned and the document cannot be viewed inline.

        The `media` PDF is capped at a maximum page count, so `pagesTruncated`,
        `truncatedFrom` and `originalPageCount` tell the viewer whether and where pages
        are missing.
      tags:
        - Assets
      responses:
        '200':
          description: Signed view URL and document page metadata.
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    description: 'Signed GET URL (`Content-Type: application/pdf`, `Content-Disposition: inline`).'
                  expires:
                    type: integer
                    format: int64
                    description: Unix timestamp (seconds) when the signed URL expires.
                    example: 1705988395
                  pageCount:
                    type: integer
                    description: Number of pages in the served (possibly truncated) PDF.
                  pagesTruncated:
                    type: boolean
                    description: True when the served PDF holds only part of the original document.
                  truncatedFrom:
                    type: string
                    enum:
                      - start
                      - end
                    description: Which end of the document the served pages were kept from.
                  originalPageCount:
                    type: integer
                    description: Page count of the original document.
        '400':
          description: |
            `assetNotFound` (asset missing or not active), `invalidAssetType` (not a document),
            or `fileNotFound` (no active `media` file yet).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - bearerAuth: []
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the document asset to view.
      x-request-source: joi
  /assets/{assetId}/promote-to-project:
    post:
      summary: Promote a chat-message attachment to a project asset
      description: |
        Copies a chat-message attachment (an asset with `ownerResourceType: 'chat'` and
        `functionType: 'attachment'`) into the destination project as a new, fully
        independent project asset. Deleting either side afterwards has no effect on the other.

        Permission is `canCreateAsset` on the destination project (`body.projectId`), which
        excludes reviewers. The source chat must live in the same workspace as the
        destination project.

        Idempotent: promoting the same source into the same project again returns the
        existing promoted asset with `deduped: true`. When post-processing is required the
        new asset is returned before it is `active`; the usual asset activation notifications
        fire when processing completes.
      tags:
        - Assets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                projectId:
                  type: string
                  format: uuid
                  description: Destination project.
                fileName:
                  type: string
                  minLength: 1
                  maxLength: 255
                  description: Display name for the new project asset. Defaults to the source attachment's name.
              required:
                - projectId
              additionalProperties: false
      responses:
        '200':
          description: The promoted project asset.
          content:
            application/json:
              schema:
                type: object
                properties:
                  asset:
                    $ref: '#/components/schemas/Asset'
                  deduped:
                    type: boolean
                    description: >-
                      True when an earlier promote of the same source into the same project was returned instead of
                      creating a new asset.
        '400':
          description: |
            `assetNotFound` (source missing), `attachmentNotPromotable` (source is not an
            active chat-message attachment), or a validation error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: |
            Caller lacks `canCreateAsset` on the destination project, or
            `attachmentPromotionWorkspaceMismatch` (source chat and destination project are
            in different workspaces).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
        '500':
          description: '`attachmentPromotionFailed` — the copy failed mid-way; the partial asset is marked for deletion.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the chat-message attachment asset to promote.
      x-request-source: joi
  /boards:
    post:
      summary: Create a new board
      description: >-
        Creates a new kanban board for a project with default or custom columns. Default columns are Backlog, To Do, In
        Progress, In Review, and Done.
      tags:
        - Boards
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                projectId:
                  type: string
                  format: uuid
                  description: The project this board belongs to
                name:
                  type: string
                  maxLength: 100
                  description: Board name
                description:
                  type: string
                  maxLength: 500
                  nullable: true
                  description: Board description
                visibility:
                  type: array
                  items:
                    type: string
                    enum:
                      - creator
                      - reviewer
                  minItems: 1
                  maxItems: 2
                  default:
                    - creator
                  description: Who can see this board. Must include `creator` (`['reviewer']` alone is rejected).
                columns:
                  type: array
                  items:
                    type: object
                    properties:
                      name:
                        type: string
                        maxLength: 50
                      description:
                        type: string
                        maxLength: 500
                        nullable: true
                      color:
                        type: string
                        nullable: true
                        enum:
                          - '#6B7280'
                          - '#3B82F6'
                          - '#F59E0B'
                          - '#8B5CF6'
                          - '#10B981'
                          - '#EF4444'
                          - '#F97316'
                          - '#EC4899'
                          - '#14B8A6'
                          - '#6366F1'
                          - '#84CC16'
                          - '#06B6D4'
                        description: Hex color from approved column colors
                      isDefault:
                        type: boolean
                        default: false
                        description: Whether new tasks are placed here by default
                      taskStatus:
                        type: string
                        nullable: true
                        enum:
                          - pending
                          - inProgress
                          - complete
                          - closed
                        description: When set, tasks moved into this column have their status set to this value
                      sortOrder:
                        type: number
                      reviewersCanContribute:
                        type: boolean
                        description: When true, reviewer-role users can create tasks in this column on a reviewer-visibility board
                    required:
                      - name
                    additionalProperties: false
                  description: Custom columns (omit for defaults)
              required:
                - projectId
                - name
              additionalProperties: false
      responses:
        '201':
          description: Board created successfully with columns
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Board'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters: []
      x-request-source: joi
  /boards/project/{projectId}:
    get:
      summary: Get boards for a project
      description: >-
        Returns all active boards for a project, filtered by the user's visibility role. Each board includes its
        columns. Optional search and tags filters.
      tags:
        - Boards
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The project to get boards for
        - name: search
          in: query
          required: false
          schema:
            type: string
            maxLength: 500
          description: Case-insensitive substring match on board name
        - name: visibility
          in: query
          required: false
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Narrow results to a specific visibility (must be a subset of what the caller can already access)
        - name: tags
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
              format: uuid
            x-accepts-single-value: true
          style: form
          explode: true
          description: Filter by one or more tag IDs (matches boards containing any of the supplied tags)
      responses:
        '200':
          description: List of boards with columns
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Board'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /boards/{boardId}:
    get:
      summary: Get a board with columns and tasks
      description: >-
        Returns a board with all its columns and tasks grouped by column. Tasks are populated with creator, assignedTo,
        and project.
      tags:
        - Boards
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Board with columns and grouped tasks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BoardWithTasks'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the board
      x-request-source: joi
    put:
      summary: Update a board
      description: |
        Update board name, description, visibility, status, or sort order. Requires
        `canUpdate{Creator|Reviewer}Board` for the board's visibility.

        Narrowing `visibility` when tasks, assignees or task relations depend on the
        removed tier returns 409 `boardVisibilityChangeBlocked`. Re-issue the request with
        `cascade: true` to apply the cleanup automatically (re-stamp tasks, unassign blocked
        assignees, delete blocked relations); the applied cleanup is reported in the
        response's `cascade` field.
      tags:
        - Boards
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 100
                description:
                  type: string
                  maxLength: 500
                  nullable: true
                visibility:
                  type: array
                  items:
                    type: string
                    enum:
                      - creator
                      - reviewer
                  minItems: 1
                  maxItems: 2
                  description: Must include `creator`.
                status:
                  type: string
                  enum:
                    - active
                    - archived
                sortOrder:
                  type: number
                cascade:
                  type: boolean
                  description: Opt in to automatic cleanup when narrowing visibility would otherwise be blocked.
              additionalProperties: false
              minProperties: 1
      responses:
        '200':
          description: Board updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Board'
                  - type: object
                    properties:
                      cascade:
                        type: object
                        nullable: true
                        description: 'Cleanup applied because `cascade: true` was passed; `null` when no cascade ran.'
                        properties:
                          restampedTaskIds:
                            type: array
                            items:
                              type: string
                              format: uuid
                          unassignedTasks:
                            type: array
                            items:
                              type: object
                              properties:
                                taskId:
                                  type: string
                                  format: uuid
                                assignedToId:
                                  type: string
                                  format: uuid
                          removedRelationIds:
                            type: array
                            items:
                              type: string
                              format: uuid
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Board not found (`boardNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >-
            `boardVisibilityChangeBlocked` — removing the visibility would orphan tasks, assignees or related chats;
            retry with `cascade: true`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the board
      x-request-source: joi
    delete:
      summary: Delete a board
      description: |
        Deletes a board and its columns. The optional body controls what happens
        to tasks on the board:

          - `unassign` (default) — set `boardId` and `columnId` to null on each task. Tasks are kept.
          - `delete` — delete every task on the board, plus its TaskRelation, TaskLink, and TaskEvent rows.
          - `reassign` — move tasks to `targetBoardId` / `targetColumnId` (defaults to the target's default column). If the target column has a `taskStatus`, the task's `status` is updated to match.

        For `reassign`, the caller must also have Update access on the target board's tasks
        (`targetBoardRequired` when `targetBoardId` is missing, `targetBoardSameAsSource`
        when it equals the board being deleted). Requires `canDelete{Creator|Reviewer}Board`.
      tags:
        - Boards
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                disposition:
                  type: string
                  enum:
                    - unassign
                    - delete
                    - reassign
                targetBoardId:
                  type: string
                  format: uuid
                  description: Required when disposition is 'reassign'.
                targetColumnId:
                  type: string
                  format: uuid
                  description: Optional. Defaults to the target board's default column.
              additionalProperties: false
      responses:
        '200':
          description: Board deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  disposition:
                    type: string
                    enum:
                      - unassign
                      - delete
                      - reassign
                  deletedTaskIds:
                    type: array
                    items:
                      type: string
                      format: uuid
                  reassignedTaskIds:
                    type: array
                    items:
                      type: string
                      format: uuid
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the board
      x-request-source: joi
  /boards/{boardId}/columns:
    post:
      summary: Add a column to a board
      description: Adds a new column to the board. If sortOrder is not provided, it is placed after the last column.
      tags:
        - Board Columns
      security:
        - bearerAuth: []
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 50
                description:
                  type: string
                  maxLength: 500
                  nullable: true
                color:
                  type: string
                  nullable: true
                  enum:
                    - '#6B7280'
                    - '#3B82F6'
                    - '#F59E0B'
                    - '#8B5CF6'
                    - '#10B981'
                    - '#EF4444'
                    - '#F97316'
                    - '#EC4899'
                    - '#14B8A6'
                    - '#6366F1'
                    - '#84CC16'
                    - '#06B6D4'
                isDefault:
                  type: boolean
                  default: false
                taskStatus:
                  type: string
                  nullable: true
                  enum:
                    - pending
                    - inProgress
                    - complete
                    - closed
                  description: When set, tasks moved into this column have their status set to this value
                sortOrder:
                  type: number
                reviewersCanContribute:
                  type: boolean
                  description: When true, reviewer-role users can create tasks in this column on a reviewer-visibility board
              required:
                - name
              additionalProperties: false
      responses:
        '201':
          description: Column created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BoardColumn'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /boards/{boardId}/columns/reorder:
    put:
      summary: Reorder columns
      description: Updates the sortOrder of multiple columns at once.
      tags:
        - Board Columns
      security:
        - bearerAuth: []
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                columns:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      sortOrder:
                        type: number
                    required:
                      - id
                      - sortOrder
                    additionalProperties: false
                  minItems: 1
              required:
                - columns
              additionalProperties: false
      responses:
        '200':
          description: Columns reordered successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/BoardColumn'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /boards/{boardId}/columns/{columnId}:
    put:
      summary: Update a column
      description: Update column name, description, color, isDefault, taskStatus, sortOrder, or reviewersCanContribute.
      tags:
        - Board Columns
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 50
                description:
                  type: string
                  maxLength: 500
                  nullable: true
                color:
                  type: string
                  nullable: true
                  enum:
                    - '#6B7280'
                    - '#3B82F6'
                    - '#F59E0B'
                    - '#8B5CF6'
                    - '#10B981'
                    - '#EF4444'
                    - '#F97316'
                    - '#EC4899'
                    - '#14B8A6'
                    - '#6366F1'
                    - '#84CC16'
                    - '#06B6D4'
                isDefault:
                  type: boolean
                taskStatus:
                  type: string
                  nullable: true
                  enum:
                    - pending
                    - inProgress
                    - complete
                    - closed
                sortOrder:
                  type: number
                reviewersCanContribute:
                  type: boolean
              additionalProperties: false
              minProperties: 1
      responses:
        '200':
          description: Column updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BoardColumn'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: columnId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      x-request-source: joi
    delete:
      summary: Delete a column
      description: >-
        Deletes a column and moves its tasks to `targetColumnId` when given, otherwise to the default column, otherwise
        to any remaining column. Deleting the last column is rejected (`invalidBoard`).
      tags:
        - Board Columns
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                targetColumnId:
                  type: string
                  format: uuid
                  description: Column on the same board to move the deleted column's tasks into.
              additionalProperties: false
      responses:
        '200':
          description: Column deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Column deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: columnId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      x-request-source: joi
  /boards/{boardId}/tasks:
    get:
      summary: Get tasks for a board
      description: >-
        Returns all tasks on a board, optionally filtered by column, status, assignee, search, or tags. Tasks are
        populated with creator, assignedTo, and project. API tokens need the `tasks:read` scope.
      tags:
        - Board Tasks
      security:
        - bearerAuth: []
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: columnId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter tasks by column
        - name: search
          in: query
          required: false
          schema:
            type: string
            maxLength: 500
          description: Case-insensitive substring match on task subject
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - pending
              - inProgress
              - complete
              - closed
          description: Filter by task status
        - name: assignedToId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by assignee
        - name: tags
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
              format: uuid
            x-accepts-single-value: true
          style: form
          explode: true
          description: Filter by one or more tag IDs (matches tasks containing any of the supplied tags)
      responses:
        '200':
          description: List of tasks
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Task'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
    post:
      summary: Create a task on a board
      description: |
        Creates a new task and places it on the board. If no columnId is specified, the task
        is placed in the default column. Auto-assigns an incrementing taskNumber. API tokens
        need the `tasks:write` scope. Reviewers can only create tasks in columns flagged
        `reviewersCanContribute` (403 `reviewerCannotContributeHere`); an assignee who cannot
        see the board's visibility is rejected (400 `invalidAssignee`).

        When `announce` is provided, an assistant reply carrying the new task is posted in
        `announce.chatId` as a reply to `announce.messageId`; announce failures never fail
        the request.
      tags:
        - Board Tasks
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                subject:
                  type: string
                  maxLength: 500
                description:
                  type: string
                  maxLength: 20000
                  nullable: true
                assignedToId:
                  type: string
                  format: uuid
                  description: User to assign the task to
                columnId:
                  type: string
                  format: uuid
                  description: Column to place the task in (defaults to the default column)
                announce:
                  type: object
                  properties:
                    chatId:
                      type: string
                      format: uuid
                    messageId:
                      type: string
                      format: uuid
                  required:
                    - chatId
                    - messageId
                  additionalProperties: false
              required:
                - subject
              additionalProperties: false
      responses:
        '201':
          description: Task created on board
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      x-request-source: joi
  /boards/{boardId}/tasks/add:
    post:
      summary: Add an existing task to a board
      description: Links an existing project task to a board column. If the task does not have a taskNumber, one is assigned.
      tags:
        - Board Tasks
      security:
        - bearerAuth: []
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                taskId:
                  type: string
                  format: uuid
                  description: The existing task to add
                columnId:
                  type: string
                  format: uuid
                  description: Column to place the task in (defaults to the default column)
              required:
                - taskId
              additionalProperties: false
      responses:
        '200':
          description: Task added to board
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /boards/{boardId}/tasks/{taskId}/move:
    put:
      summary: Move a task to a different column (or board)
      description: |
        Moves a task to a new column and optionally sets its position. Pass `targetBoardId`
        to move the task onto another board in the same project; `columnId` then defaults to
        the target board's default column and the caller must also hold `canMove*BoardTask`
        for the target board's visibility (403 otherwise). Without `targetBoardId`,
        `columnId` is required.

        If the destination column has a `taskStatus`, the task's `status` is set to that
        value; otherwise the status is left unchanged.
      tags:
        - Board Tasks
      security:
        - bearerAuth: []
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                targetBoardId:
                  type: string
                  format: uuid
                  description: Move the task to this board instead of staying on `{boardId}`.
                columnId:
                  type: string
                  format: uuid
                  x-conditionally-required: true
                  description: >-
                    Target column. Required unless `targetBoardId` is given (then defaults to the target board's default
                    column).
                sortOrder:
                  type: number
                  description: Position within the column
              additionalProperties: false
      responses:
        '200':
          description: Task moved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /boards/{boardId}/tasks/{taskId}:
    delete:
      summary: Remove a task from a board
      description: Unlinks a task from the board by setting boardId and columnId to null. The task itself is not deleted.
      tags:
        - Board Tasks
      security:
        - bearerAuth: []
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Task removed from board
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /tasks/{taskId}/details:
    put:
      summary: Update task details
      description: >-
        Update a task's subject, description, or assignee. Used for editing task content outside of board column
        operations.
      tags:
        - Task Details
      security:
        - bearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                subject:
                  type: string
                  maxLength: 500
                description:
                  type: string
                  maxLength: 20000
                  nullable: true
                assignedToId:
                  type: string
                  format: uuid
                  nullable: true
                  description: Set to null to unassign
              additionalProperties: false
              minProperties: 1
      responses:
        '200':
          description: Task updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /tasks/{taskId}/links:
    post:
      summary: Link two tasks
      description: Creates a relationship link between two tasks. Duplicate links are rejected.
      tags:
        - Task Links
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                linkedTaskId:
                  type: string
                  format: uuid
                  description: The task to link to
                linkType:
                  type: string
                  default: related
                  enum:
                    - related
                    - blocks
                    - blockedBy
                    - duplicate
              required:
                - linkedTaskId
              additionalProperties: false
      responses:
        '201':
          description: Task link created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskLink'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: '`taskLinkAlreadyExists` — these tasks are already linked.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      x-request-source: joi
    get:
      summary: Get links for a task
      description: >-
        Returns all links for a task in both directions. Link types are normalized relative to the queried task (e.g.,
        blocks/blockedBy are flipped when the task is in the linkedTaskId position).
      tags:
        - Task Links
      security:
        - bearerAuth: []
      responses:
        '200':
          description: List of task links with populated linked tasks
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskLink'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      x-request-source: joi
  /tasks/{taskId}/links/{linkedTaskId}:
    delete:
      summary: Unlink two tasks
      description: Removes the link between two tasks in both directions.
      tags:
        - Task Links
      security:
        - bearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: linkedTaskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Task link removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Tasks unlinked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /boards/{boardId}/follow:
    put:
      summary: Follow a board
      description: >-
        Add the current user to the board's followers list. Idempotent — following again has no effect. Board followers
        receive email notifications when new tasks are created.
      tags:
        - Board Following
      security:
        - bearerAuth: []
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Board with updated followers list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Board'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /boards/{boardId}/unfollow:
    put:
      summary: Unfollow a board
      description: Remove the current user from the board's followers list.
      tags:
        - Board Following
      security:
        - bearerAuth: []
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Board with updated followers list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Board'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /boards/{boardId}/tag:
    put:
      summary: Tag a board
      description: Add a project tag to a board. Idempotent — tagging again has no effect.
      tags:
        - Boards
      security:
        - bearerAuth: []
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
                  description: ID of the project tag to add
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Board with updated tags
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Board'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /boards/{boardId}/untag:
    put:
      summary: Untag a board
      description: Remove a project tag from a board.
      tags:
        - Boards
      security:
        - bearerAuth: []
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Board with updated tags
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Board'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /boards/project/{projectId}/tasks:
    get:
      summary: Search tasks across all boards in a project
      description: |
        Returns paginated tasks across all boards in a project. Supports search,
        board/status/assignee/tag filters. By default only tasks that belong to a
        board are returned; pass `boardId=unassigned` to get the orphaned (no
        board) tasks instead.
      tags:
        - Board Tasks
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: search
          in: query
          required: false
          schema:
            type: string
            maxLength: 500
          description: |
            OR-matched search. Always matches against `subject` (case-insensitive substring).
            Additionally matches the exact `taskNumber` when the query is purely numeric,
            and the exact task `id` when the query is a UUID (or legacy 24-char hex ObjectId).
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - pending
              - inProgress
              - complete
              - closed
        - name: boardId
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: uuid
              - type: string
                enum:
                  - unassigned
          description: Filter by specific board ID, or pass the literal string `unassigned` to return only tasks with no board.
        - name: columnId
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: assignedToId
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: visibility
          in: query
          required: false
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: >-
            Must be a visibility the caller can see (403 otherwise). When omitted and the caller can only see one tier,
            results are narrowed to it.
        - name: tags
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
              format: uuid
            x-accepts-single-value: true
          style: form
          explode: true
          description: Filter by one or more tag IDs (matches tasks containing any of the supplied tags)
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
          style: deepObject
          explode: true
          description: Sort order (defaults to createdAt descending)
      responses:
        '200':
          description: Paginated list of tasks across boards (populated with creator and assignedTo)
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Task'
                  page:
                    type: integer
                  limit:
                    type: integer
                  totalPages:
                    type: integer
                  totalResults:
                    type: integer
                  hasNextPage:
                    type: boolean
                  hasPrevPage:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /workspaces/{workspaceId}/bots:
    post:
      summary: Create a bot user in a workspace
      description: >-
        Creates a bot user, assigns workspace membership with the supplied roles, and issues a single API key. The raw
        key is returned **once** in the response and is never retrievable again — store it immediately.
      tags:
        - Bots
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Workspace ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 50
                  description: Display name for the bot
                color:
                  type: string
                  enum:
                    - '#37474F'
                    - '#FF5722'
                    - '#2962FF'
                    - '#33691E'
                    - '#00796B'
                    - '#455A64'
                    - '#2979FF'
                    - '#827717'
                    - '#7986CB'
                    - '#8E24AA'
                    - '#9575CD'
                    - '#BF360C'
                    - '#01579B'
                    - '#EF6C00'
                    - '#AA00FF'
                    - '#F44336'
                    - '#7C4DFF'
                    - '#E65100'
                    - '#8D6E63'
                    - '#283593'
                    - '#607D8B'
                    - '#009688'
                    - '#FF5252'
                    - '#03A9F4'
                    - '#C2185B'
                    - '#00ACC1'
                    - '#E91E63'
                    - '#5D4037'
                    - '#78909C'
                    - '#1E88E5'
                    - '#D500F9'
                    - '#7E57C2'
                    - '#5C6BC0'
                    - '#558B2F'
                    - '#2E7D32'
                    - '#F50057'
                    - '#004D40'
                    - '#0D47A1'
                    - '#C51162'
                    - '#D50000'
                    - '#6200EA'
                    - '#00BCD4'
                    - '#0277BD'
                  description: Hex color from the approved palette. Auto-assigned if omitted.
                roles:
                  type: array
                  items:
                    type: string
                    enum:
                      - workspaceAdmin
                      - workspaceMember
                      - workspaceChatMember
                  minItems: 1
                  description: Workspace roles to assign to the bot
                scopes:
                  type: array
                  items:
                    type: string
                    enum:
                      - chat:read
                      - chat:write
                      - tasks:read
                      - tasks:write
                      - assets:read
                      - assets:write
                      - projects:read
                      - workspaces:read
                  description: >-
                    Token scopes granted to the bot's API key. Defaults to `[]` when omitted — an empty-scope key still
                    authenticates, but receives 403 `tokenScopeMissing` on any route that requires a token scope. Keys
                    are immutable; to add scopes later, rotate the key.
              required:
                - name
                - roles
              additionalProperties: false
              example:
                name: Release Bot
                color: '#37474F'
                roles:
                  - workspaceAdmin
                scopes:
                  - chat:read
                  - chat:write
      responses:
        '201':
          description: Bot user created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  bot:
                    allOf:
                      - $ref: '#/components/schemas/BotUser'
                      - type: object
                        properties:
                          apiKey:
                            $ref: '#/components/schemas/BotApiKeySummary'
                  rawKey:
                    type: string
                    description: |
                      The full API key in the format `nrm_bot_{8-char-prefix}_{32-char-secret}`.
                      Returned once at creation and never again; only a hash is stored, so the
                      key cannot be recovered later.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Forbidden. Caller lacks `canManageBots`, or the workspace has reached the `botLimitReached` cap.
      x-request-source: joi
    get:
      summary: List bot users in a workspace
      tags:
        - Bots
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Bot users in the workspace, each with its workspace roles and project memberships.
          content:
            application/json:
              schema:
                type: array
                items:
                  allOf:
                    - $ref: '#/components/schemas/BotUser'
                    - type: object
                      properties:
                        roles:
                          type: array
                          items:
                            type: string
                          description: Workspace roles held by the bot.
                        membershipId:
                          type: string
                          format: uuid
                        projectMemberships:
                          type: array
                          items:
                            $ref: '#/components/schemas/BotProjectMembership'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /workspaces/{workspaceId}/bots/{botId}:
    get:
      summary: Get a single bot user
      description: Returns bot profile, workspace roles, and API key metadata (no secrets).
      tags:
        - Bots
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: botId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Bot details.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/BotUser'
                  - type: object
                    properties:
                      roles:
                        type: array
                        items:
                          type: string
                      membershipId:
                        type: string
                        format: uuid
                        nullable: true
                      apiKeys:
                        type: array
                        description: Active API keys (no secrets).
                        items:
                          allOf:
                            - $ref: '#/components/schemas/BotApiKeySummary'
                            - type: object
                              properties:
                                lastUsed:
                                  type: string
                                  format: date-time
                                  nullable: true
                                expiresAt:
                                  type: string
                                  format: date-time
                                  nullable: true
                                status:
                                  type: string
                                  enum:
                                    - active
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      x-request-source: joi
    put:
      summary: Update a bot user
      description: Update the bot's display name and/or color. At least one field must be supplied.
      tags:
        - Bots
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: botId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 50
                color:
                  type: string
                  enum:
                    - '#37474F'
                    - '#FF5722'
                    - '#2962FF'
                    - '#33691E'
                    - '#00796B'
                    - '#455A64'
                    - '#2979FF'
                    - '#827717'
                    - '#7986CB'
                    - '#8E24AA'
                    - '#9575CD'
                    - '#BF360C'
                    - '#01579B'
                    - '#EF6C00'
                    - '#AA00FF'
                    - '#F44336'
                    - '#7C4DFF'
                    - '#E65100'
                    - '#8D6E63'
                    - '#283593'
                    - '#607D8B'
                    - '#009688'
                    - '#FF5252'
                    - '#03A9F4'
                    - '#C2185B'
                    - '#00ACC1'
                    - '#E91E63'
                    - '#5D4037'
                    - '#78909C'
                    - '#1E88E5'
                    - '#D500F9'
                    - '#7E57C2'
                    - '#5C6BC0'
                    - '#558B2F'
                    - '#2E7D32'
                    - '#F50057'
                    - '#004D40'
                    - '#0D47A1'
                    - '#C51162'
                    - '#D50000'
                    - '#6200EA'
                    - '#00BCD4'
                    - '#0277BD'
              additionalProperties: false
              minProperties: 1
              example:
                name: Release Bot v2
                color: '#FF5722'
      responses:
        '200':
          description: Bot updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BotUser'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      x-request-source: joi
    delete:
      summary: Delete a bot user
      description: Revokes all API keys for the bot, removes the workspace membership, and marks the user `inactive`.
      tags:
        - Bots
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: botId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Bot deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      x-request-source: joi
  /workspaces/{workspaceId}/bots/{botId}/rotate-key:
    post:
      summary: Rotate the bot's API key
      description: Revokes the bot's current active API key and issues a new one. The new raw key is returned once.
      tags:
        - Bots
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: botId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: New key issued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  apiKey:
                    type: object
                    properties:
                      id:
                        type: string
                      keyPrefix:
                        type: string
                      name:
                        type: string
                      createdAt:
                        type: string
                        format: date-time
                  rawKey:
                    type: string
                    description: The new full API key. Returned once.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Bot has no active API key to rotate.
      x-request-source: joi
  /workspaces/{workspaceId}/bots/{botId}/avatar:
    post:
      summary: Set the bot's avatar
      description: >-
        Creates an avatar asset for the bot and returns signed multipart upload URLs; the caller then uploads the image
        bytes directly to those URLs. The returned `user` carries the new (still `pendingUpload`) avatar asset so the
        client can render it optimistically; the processed thumbnail arrives via the `userAvatarUpdate` WebSocket event.
      tags:
        - Bots
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: botId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: File name including extension.
                sizeInMB:
                  type: number
                  minimum: 0
                  description: File size in MB; drives the number of signed part URLs.
                checksum:
                  type: string
                  description: Hash of the file content.
              required:
                - name
                - sizeInMB
                - checksum
              additionalProperties: false
      responses:
        '200':
          description: Avatar asset created; upload the bytes to `signedUrlData`.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/AssetAndSignedLink'
                  - type: object
                    properties:
                      user:
                        $ref: '#/components/schemas/BotUser'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Bot not found in this workspace.
      x-request-source: joi
  /workspaces/{workspaceId}/bots/{botId}/memberships:
    get:
      summary: List the bot's project memberships
      description: Project memberships of the bot, limited to projects that belong to this workspace.
      tags:
        - Bots
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: botId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Project memberships.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/BotProjectMembership'
        '404':
          description: Bot not found in this workspace.
      x-request-source: joi
  /workspaces/{workspaceId}/bots/{botId}/memberships/project/{projectId}:
    put:
      summary: Add the bot to a project or replace its project roles
      description: >-
        Upserts the bot's membership on the project. If a membership already exists its role list is replaced.
        `projectOwner` and `reviewerAdmin` cannot be granted to bots.
      tags:
        - Bots
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                roles:
                  type: array
                  items:
                    type: string
                    enum:
                      - creator
                      - reviewer
                      - projectAdmin
                  minItems: 1
              required:
                - roles
              additionalProperties: false
      responses:
        '200':
          description: Membership created or updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BotProjectMembership'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Bot not found in this workspace, or project not found in this workspace.
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: botId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Must belong to `workspaceId`.
      x-request-source: joi
    delete:
      summary: Remove the bot from a project
      tags:
        - Bots
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Membership removed (also succeeds when no membership existed).
        '404':
          description: Bot not found in this workspace, or project not found in this workspace.
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: botId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Must belong to `workspaceId`.
      x-request-source: joi
  /chats/topic:
    post:
      summary: Create a new topic chat
      tags:
        - Chats
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                topicType:
                  type: string
                  enum:
                    - project
                    - asset
                topicId:
                  type: string
                  format: uuid
                subject:
                  type: string
                  maxLength: 100
                visibility:
                  type: string
              additionalProperties: false
              description: >-
                No field is enforced as required by validation, but `topicType` and `topicId` are needed to create a
                chat.
      responses:
        '200':
          description: Topic chat created successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Chat'
      security:
        - bearerAuth: []
      parameters: []
      x-request-source: joi
  /chats/member:
    post:
      summary: Create a new member chat.
      tags:
        - Chats
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                scopeType:
                  type: string
                  enum:
                    - workspace
                scopeId:
                  type: string
                  format: uuid
                subject:
                  type: string
                  maxLength: 100
                memberIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                color:
                  type: string
                  enum:
                    - '#37474F'
                    - '#FF5722'
                    - '#2962FF'
                    - '#33691E'
                    - '#00796B'
                    - '#455A64'
                    - '#2979FF'
                    - '#827717'
                    - '#7986CB'
                    - '#8E24AA'
                    - '#9575CD'
                    - '#BF360C'
                    - '#01579B'
                    - '#EF6C00'
                    - '#AA00FF'
                    - '#F44336'
                    - '#7C4DFF'
                    - '#E65100'
                    - '#8D6E63'
                    - '#283593'
                    - '#607D8B'
                    - '#009688'
                    - '#FF5252'
                    - '#03A9F4'
                    - '#C2185B'
                    - '#00ACC1'
                    - '#E91E63'
                    - '#5D4037'
                    - '#78909C'
                    - '#1E88E5'
                    - '#D500F9'
                    - '#7E57C2'
                    - '#5C6BC0'
                    - '#558B2F'
                    - '#2E7D32'
                    - '#F50057'
                    - '#004D40'
                    - '#0D47A1'
                    - '#C51162'
                    - '#D50000'
                    - '#6200EA'
                    - '#00BCD4'
                    - '#0277BD'
                  description: One of the platform's approved hex colours.
              additionalProperties: false
              description: >
                Only workspace ("Team Chat") member chats may be created; project-scoped member chats are deprecated. No
                field is enforced as required by validation, but `scopeType`, `scopeId` and `memberIds` are needed to
                create a chat. When `color` is omitted a random approved color is assigned.
      responses:
        '200':
          description: Private chat created successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMember'
        '400':
          description: Bad request due to validation failure.
      security:
        - bearerAuth: []
      parameters: []
      x-request-source: joi
    get:
      summary: Get a paginated list of member chats the user has created or is part of.
      tags:
        - Chats
      parameters:
        - name: scopeId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only return member chats belonging to this scope (workspace or project).
        - name: subjectSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
          description: Search term to filter chats by subject (partial match, case-insensitive).
        - name: memberSearch
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Member ID to filter chats by specific member participation.
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination type to use
        - name: updatedBefore
          in: query
          required: false
          schema:
            type: string
            format: date-time
            x-conditionally-required: true
          description: Get chats updated before this timestamp (only for index pagination).
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              updatedAt: -1
          style: deepObject
          explode: true
          description: Sorting criteria for chats based on creation or update times.
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 20
            default: 20
          description: Limit the number of chats returned.
        - name: recentMessages
          in: query
          required: false
          schema:
            type: string
            maxLength: 20
            default: 20
          description: Limit the number of recent messages returned for each chat.
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number for index pagination.
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only for cursor pagination)
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only for cursor pagination)
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only for cursor pagination)
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include cursor record in results (only for cursor pagination)
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only for cursor pagination)
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include the startAt record in the results (only for cursor pagination)
        - name: archived
          in: query
          required: false
          schema:
            type: boolean
          description: >
            Archive filter for the current user. `true` returns only chats the user has archived, `false` returns only
            chats the user has not archived. Omit to return both.
      responses:
        '200':
          description: List of member chats retrieved successfully.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/IndexPaginatedChatMembers'
                  - $ref: '#/components/schemas/CursorPaginatedChatMembers'
        '400':
          description: Bad request due to validation failure.
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/member/{chatId}:
    get:
      summary: Get member chat by id.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successfully retrieved member chat details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMember'
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
    put:
      summary: Update member chat subject and/or color.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                subject:
                  type: string
                  maxLength: 100
                  description: The new subject for the chat.
                color:
                  type: string
                  enum:
                    - '#37474F'
                    - '#FF5722'
                    - '#2962FF'
                    - '#33691E'
                    - '#00796B'
                    - '#455A64'
                    - '#2979FF'
                    - '#827717'
                    - '#7986CB'
                    - '#8E24AA'
                    - '#9575CD'
                    - '#BF360C'
                    - '#01579B'
                    - '#EF6C00'
                    - '#AA00FF'
                    - '#F44336'
                    - '#7C4DFF'
                    - '#E65100'
                    - '#8D6E63'
                    - '#283593'
                    - '#607D8B'
                    - '#009688'
                    - '#FF5252'
                    - '#03A9F4'
                    - '#C2185B'
                    - '#00ACC1'
                    - '#E91E63'
                    - '#5D4037'
                    - '#78909C'
                    - '#1E88E5'
                    - '#D500F9'
                    - '#7E57C2'
                    - '#5C6BC0'
                    - '#558B2F'
                    - '#2E7D32'
                    - '#F50057'
                    - '#004D40'
                    - '#0D47A1'
                    - '#C51162'
                    - '#D50000'
                    - '#6200EA'
                    - '#00BCD4'
                    - '#0277BD'
                  description: One of the platform's approved hex colours.
              additionalProperties: false
      responses:
        '200':
          description: Successfully updated member chat subject.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMember'
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
    delete:
      summary: >-
        Mark member chat for deletion. From the user's perspective the chat will have been deleted. Only chat creators
        may delete a chat.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Chat successfully marked as deleted.
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/member/{chatId}/icon:
    put:
      summary: Upload and set member chat icon.
      description: >
        Creates an icon asset for the member chat and returns signed upload links for it. Any existing icon is marked
        for deletion. Requires member-chat management permission (`canManageMemberChat`).
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the member chat
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 100
                  description: File name with extension
                  example: icon.png
                checksum:
                  type: string
                  description: MD5 or SHA-256 hash of the file
                  example: d9729feb74992cc3482b350163a1a010
                sizeInMB:
                  type: number
                  maximum: 10
                  description: File size in megabytes
                  example: 0.25
              required:
                - name
                - checksum
                - sizeInMB
              additionalProperties: false
      responses:
        '200':
          description: >-
            Icon asset created. The response is the created asset/upload-link record with the updated chat attached
            under `chat`.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/AssetAndSignedLink'
                  - type: object
                    properties:
                      chat:
                        $ref: '#/components/schemas/ChatMember'
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/member/{chatId}/archive:
    put:
      summary: Archive a member chat for the current user.
      description: >-
        Adds the current user's ID to the chat's archivedBy array. Per-user archiving does not affect other members'
        view of the chat.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Chat archived successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMember'
        '403':
          description: Forbidden - User is not a member of the chat.
        '404':
          description: Chat not found.
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/member/{chatId}/unarchive:
    put:
      summary: Unarchive a member chat for the current user.
      description: Removes the current user's ID from the chat's archivedBy array.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Chat unarchived successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMember'
        '403':
          description: Forbidden - User is not a member of the chat.
        '404':
          description: Chat not found.
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/member/addable:
    get:
      summary: Get members addable to a NEW member chat, by scope, before the chat exists.
      description: >
        Populates the create-chat member picker. Unlike the raw membership-list endpoints, this is not admin-gated — any
        user permitted to create a member chat in the scope (including a plain workspaceChatMember) may call it, and
        only chat-eligible members are returned.
      tags:
        - Chats
      parameters:
        - name: scopeType
          in: query
          required: true
          schema:
            type: string
            enum:
              - workspace
              - project
        - name: scopeId
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: A list of members addable to a member chat created in the given scope.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ProjectScopeResponse'
                  - $ref: '#/components/schemas/WorkspaceScopeResponse'
        '400':
          description: Invalid request parameters
        '403':
          description: Forbidden - User cannot create a member chat in the scope.
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/member/members/{chatId}:
    get:
      summary: Get a list of addable members to a member chat based on the chats scope and the users role in that scope.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: chatId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: A list of addable members based on the chat's scope and the user's role.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ProjectScopeResponse'
                  - $ref: '#/components/schemas/WorkspaceScopeResponse'
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
    put:
      summary: Add an array of members to a member chat by user ID.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                memberIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: Array of user IDs.
              additionalProperties: false
        description: Array of user IDs to add as members. Not enforced by validation, but the request cannot succeed without it.
      responses:
        '200':
          description: Successfully added members to the chat.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMember'
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
    delete:
      summary: Delete an array of members from a member chat by user ID.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                memberIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: Array of user IDs.
              additionalProperties: false
        description: >-
          Array of user IDs to remove from the chat. Not enforced by validation, but the request cannot succeed without
          it. The chat creator cannot be removed (`ownerCannotLeaveChat`).
      responses:
        '200':
          description: Successfully removed members from the chat.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMember'
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/{chatId}:
    get:
      summary: Get topic chat by id.
      description: >-
        Returns a topic chat (project/asset/task/public) without messages. Member chats are served by `GET
        /chats/member/{chatId}`.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successfully retrieved chat details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Chat'
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
    put:
      summary: Update chat subject.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                subject:
                  type: string
                  maxLength: 100
                  description: The new subject for the chat.
              additionalProperties: false
      responses:
        '200':
          description: Successfully updated chat subject.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Chat'
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
    delete:
      summary: Mark chat for deletion. Only chat creators may delete a chat.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Chat successfully marked as deleted.
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/{chatId}/follow:
    put:
      summary: Follow a chat to receive updates
      description: >-
        Add the authenticated user to the chat's following list. Users in the following list receive notifications about
        new messages and updates. Works with all chat types (topic, member, submission). Following a chat multiple times
        is idempotent.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the chat to follow
      responses:
        '200':
          description: Successfully followed the chat
        '400':
          description: Invalid request parameters
        '403':
          description: Forbidden - User does not have permission to access this chat
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/{chatId}/unfollow:
    put:
      summary: Unfollow a chat to stop receiving updates
      description: >-
        Remove the authenticated user from the chat's following list. Users can unfollow a chat even if they no longer
        have access to it, allowing them to stop receiving notifications. Works with all chat types (topic, member,
        submission). Unfollowing a chat you're not following is idempotent.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the chat to unfollow
      responses:
        '200':
          description: Successfully unfollowed the chat
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/{chatId}/mentionable/assets:
    get:
      summary: Get mentionable assets for a chat with pagination
      description: >
        Returns a paginated list of assets that can be mentioned in the specified chat. The assets returned depend on
        the chat type:

        - **Topic chat about a project**: Returns assets from that project with matching visibility

        - **Topic chat about an asset**: Returns assets from the asset's owner project with matching visibility

        - **Member chat with project scope**: Returns all assets from that project (all visibilities)

        - **Submission chat**: Returns all assets from the project (all visibilities)

        - **AI chat (Nu Chat)**: Returns assets from the topic's project, filtered to the
          **caller's own** visibility tiers. A workspace- or social-scoped topic has no
          project to resolve against and returns an empty array.

          Unlike every other chat type above, an AI chat has no meaningful visibility of
          its own to inherit — its backing `Chat` row is created with a fixed `creator`
          placeholder, identical for every user — so tiers are derived from the caller's
          inherited permissions instead. Owning the topic is not on its own evidence of
          creator-tier access to the project.
        - **Member chat with workspace/social scope**: Returns empty array (no assets are mentionable)


        Only active assets are returned. Supports both cursor-based and index-based pagination, plus name filtering.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the chat
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
          description: Optional partial name search filter (case-insensitive)
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              name: 1
          style: deepObject
          explode: true
          description: Sort order (1 for ascending, -1 for descending)
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Maximum number of results per page
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination type (cursor or index-based)
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for cursor-based pagination (only when paginate=cursor)
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Reverse pagination direction (only when paginate=cursor)
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include total counts (only when paginate=cursor)
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include cursor record (only when paginate=cursor)
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (only when paginate=index)
      responses:
        '200':
          description: >-
            Successfully retrieved mentionable assets. Index pagination returns a `PaginatedResult`; cursor pagination
            returns a `CursorPaginatedResult`. Both carry the matching assets in `results`.
          content:
            application/json:
              schema:
                oneOf:
                  - allOf:
                      - $ref: '#/components/schemas/PaginatedResult'
                      - type: object
                        properties:
                          results:
                            type: array
                            items:
                              $ref: '#/components/schemas/Asset'
                  - allOf:
                      - $ref: '#/components/schemas/CursorPaginatedResult'
                      - type: object
                        properties:
                          results:
                            type: array
                            items:
                              $ref: '#/components/schemas/Asset'
        '400':
          description: Invalid request parameters
        '403':
          description: Forbidden - User does not have permission to access this chat
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/{chatId}/mentionable/folders:
    get:
      summary: Get mentionable folders for a chat with pagination
      description: >
        Returns a paginated list of folders that can be mentioned in the specified chat. The folders returned depend on
        the chat type:

        - **Topic chat about a project**: Returns folders from that project with matching visibility

        - **Topic chat about an asset**: Returns folders from the asset's owner project with matching visibility

        - **Member chat with project scope**: Returns all folders from that project (all visibilities)

        - **Submission chat**: Returns reviewer-visibility folders from the project

        - **AI chat (Nu Chat)**: Returns folders from the topic's project, filtered to the
          **caller's own** visibility tiers. A workspace- or social-scoped topic has no
          project to resolve against and returns an empty array.

          Unlike every other chat type above, an AI chat has no meaningful visibility of
          its own to inherit — its backing `Chat` row is created with a fixed `creator`
          placeholder, identical for every user — so tiers are derived from the caller's
          inherited permissions instead. Owning the topic is not on its own evidence of
          creator-tier access to the project.
        - **Member chat with workspace/social scope**: Returns empty array (no folders are mentionable)


        Only active folders are returned. Supports both cursor-based and index-based pagination, plus name filtering.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the chat
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
          description: Optional partial name search filter (case-insensitive)
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              name: 1
          style: deepObject
          explode: true
          description: Sort order (1 for ascending, -1 for descending)
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Maximum number of results per page
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination type (cursor or index-based)
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for cursor-based pagination (only when paginate=cursor)
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Reverse pagination direction (only when paginate=cursor)
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include total counts (only when paginate=cursor)
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include cursor record (only when paginate=cursor)
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (only when paginate=index)
      responses:
        '200':
          description: >-
            Successfully retrieved mentionable folders. Index pagination returns a `PaginatedResult`; cursor pagination
            returns a `CursorPaginatedResult`. Both carry the matching folders in `results`.
          content:
            application/json:
              schema:
                oneOf:
                  - allOf:
                      - $ref: '#/components/schemas/PaginatedResult'
                      - type: object
                        properties:
                          results:
                            type: array
                            items:
                              $ref: '#/components/schemas/Folder'
                  - allOf:
                      - $ref: '#/components/schemas/CursorPaginatedResult'
                      - type: object
                        properties:
                          results:
                            type: array
                            items:
                              $ref: '#/components/schemas/Folder'
        '400':
          description: Invalid request parameters
        '403':
          description: Forbidden - User does not have permission to access this chat
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/{chatId}/mentionable/tasks:
    get:
      summary: Get mentionable board tasks for a chat with pagination
      description: >
        Returns a paginated list of board tasks that can be mentioned in the specified chat. Requires the workspace to
        have the `boards` capability.


        Scope/visibility rules:

        - **Topic chat about a project**: Tasks in that project on boards whose `visibility` array includes the chat's
        visibility.

        - **Topic chat about an asset**: Tasks in the asset's owner project on boards whose `visibility` array includes
        the chat's visibility.

        - **Topic chat about a task**: Sibling tasks in the parent task's project on boards whose `visibility` array
        includes the chat's visibility.

        - **Member chat with project scope**: All board tasks in that project (no visibility filter — mirrors
        asset/folder mention behaviour for this chat type).

        - **Submission chat**: Tasks in the submission's project on boards whose `visibility` array includes
        `'reviewer'`.

        - **Member chat with workspace/social scope**: Empty result — no tasks are mentionable.


        Legacy mention-only tasks (`boardId === null`) are never returned. The `nameSearch` query is OR-matched across
        `subject` substring, exact `taskNumber` (when numeric), and exact `id` (when a UUID).


        Returns `capabilityNotAvailable` / 403 when the workspace lacks the `boards` capability.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the chat
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
          description: Optional search filter — matches subject substring (case-insensitive), exact taskNumber, or exact task id
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              subject:
                type: number
                enum:
                  - 1
                  - -1
              taskNumber:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              updatedAt: -1
          style: deepObject
          explode: true
          description: Sort order (1 for ascending, -1 for descending). Defaults to most recently updated first.
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Maximum number of results per page
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination type (cursor or index-based)
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for cursor-based pagination (only when paginate=cursor)
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Reverse pagination direction (only when paginate=cursor)
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include total counts (only when paginate=cursor)
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include cursor record (only when paginate=cursor)
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (only when paginate=index)
      responses:
        '200':
          description: Successfully retrieved mentionable tasks
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          $ref: '#/components/schemas/UUID'
                        subject:
                          type: string
                        taskNumber:
                          type: integer
                        status:
                          type: string
                          enum:
                            - pending
                            - inProgress
                            - complete
                            - closed
                        projectId:
                          $ref: '#/components/schemas/UUID'
                        boardId:
                          $ref: '#/components/schemas/UUID'
                        assignedToId:
                          $ref: '#/components/schemas/UUID'
                        createdAt:
                          type: integer
                          format: int64
                        updatedAt:
                          type: integer
                          format: int64
                  page:
                    type: integer
                    description: Current page (index pagination only)
                  limit:
                    type: integer
                  totalResults:
                    type: integer
                  totalPages:
                    type: integer
                  hasNext:
                    type: boolean
                    description: Cursor pagination indicator
                  hasPrev:
                    type: boolean
                    description: Cursor pagination indicator
        '400':
          description: Invalid request parameters
        '403':
          description: >-
            Forbidden - User does not have permission to access this chat, or the workspace lacks the `boards`
            capability (`capabilityNotAvailable`)
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/{chatId}/mentionable/submissions:
    get:
      summary: Get mentionable submissions for a chat
      description: >
        Returns a paginated list of active submissions in the chat's project that can be mentioned with
        `{{submissionMention:submissionId}}` tokens.

        The project is resolved from the chat (project/asset/task topic chats, project-scoped member chats, submission
        chats and project-scoped AI chats).

        Chats without a project scope return an empty page. Index pagination only.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the chat
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
          description: Optional partial subject search filter (case-insensitive)
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              subject:
                type: number
                enum:
                  - 1
                  - -1
              lastMessageAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              createdAt: -1
          style: deepObject
          explode: true
          description: Sort order (1 for ascending, -1 for descending)
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Maximum number of results per page
        - name: page
          in: query
          required: false
          schema:
            type: number
            default: 1
          description: Page number
      responses:
        '200':
          description: Successfully retrieved mentionable submissions
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResult'
                  - type: object
                    properties:
                      results:
                        type: array
                        items:
                          $ref: '#/components/schemas/MentionableSubmission'
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/{chatId}/mentionable/publics:
    get:
      summary: Get mentionable public releases for a chat
      description: >
        Returns a paginated list of active public releases (share links) owned by the chat's project that can be
        mentioned with

        `{{publicMention:token}}` tokens. The project is resolved from the chat the same way as for mentionable
        submissions.

        Chats without a project scope return an empty page. Index pagination only.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the chat
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
          description: Optional partial title search filter (case-insensitive)
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              title:
                type: number
                enum:
                  - 1
                  - -1
              expires:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              createdAt: -1
          style: deepObject
          explode: true
          description: Sort order (1 for ascending, -1 for descending)
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Maximum number of results per page
        - name: page
          in: query
          required: false
          schema:
            type: number
            default: 1
          description: Page number
      responses:
        '200':
          description: Successfully retrieved mentionable public releases
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResult'
                  - type: object
                    properties:
                      results:
                        type: array
                        items:
                          $ref: '#/components/schemas/MentionablePublic'
        '400':
          description: Invalid request parameters
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/topic/{topicId}:
    get:
      summary: Get chat by Topic ID
      tags:
        - Chats
      parameters:
        - name: topicId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier for the topic
        - name: topicType
          in: query
          required: false
          schema:
            type: string
            enum:
              - asset
              - project
              - public
              - task
          description: Type of the chat topic
        - name: visibility
          in: query
          required: false
          schema:
            type: string
          description: Visibility of the chat
        - name: messages
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: >-
            Number of recent messages to retrieve. Note - the current handler does not forward this value, so the
            default of 10 is always applied.
        - name: replies
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Number of recent replies to retrieve
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting options for the chat
      responses:
        '200':
          description: Chat data retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Chat'
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/{chatId}/new-message:
    post:
      summary: Create a new message in the specified chat
      description: >
        Creates a message in any chat type (topic, member, submission, AI, support). Either `content` or at least one
        attachment must be provided. API tokens require the `chat:write` scope (`tokenScopeMissing` / 403 otherwise).
        Mentioning users in topic/member/submission chats creates tasks and notifications for them.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the chat
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
                  maxLength: 10000
                attachments:
                  type: array
                  items:
                    $ref: '#/components/schemas/IdNameOrScratchIdNameUnion'
                  maxItems: 6
                  description: >
                    Attachments for a new message. Two item shapes are accepted, distinguished by the presence of
                    `scratchId`: the upload shape describes new bytes the client will upload via the signed URLs
                    returned in `attachmentData`; the scratch shape references bytes already staged on Scratch (e.g. an
                    AI Revision result) which are promoted to chat-scoped assets when the message lands.
                replyToId:
                  type: string
                  format: uuid
                annotations:
                  type: array
                  items:
                    oneOf:
                      - $ref: '#/components/schemas/DotAnnotationInput'
                      - $ref: '#/components/schemas/FrameCommentAnnotationInput'
                      - $ref: '#/components/schemas/ShapeAnnotationInput'
                      - $ref: '#/components/schemas/TextAnnotationInput'
                      - $ref: '#/components/schemas/PathAnnotationInput'
                      - $ref: '#/components/schemas/Dot3dAnnotationInput'
                    title: AnnotationInput
                  maxItems: 100
                  description: Annotations associated with the message; each is either a Dot or a FrameComment.
                mentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: Array of user IDs mentioned in the message using {{mention:userId}} tokens
                assetMentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: >-
                    Array of asset IDs mentioned in the message using {{assetMention:assetId}} tokens. In responses,
                    these are populated with full Asset objects.
                folderMentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: >-
                    Array of folder IDs mentioned in the message using {{folderMention:folderId}} tokens. In responses,
                    these are populated with full Folder objects.
                submissionMentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: Array of submission IDs mentioned in the message using {{submissionMention:submissionId}} tokens.
                publicMentions:
                  type: array
                  items:
                    type: string
                  maxItems: 10
                  default: []
                  description: Array of public release tokens mentioned in the message using {{publicMention:token}} tokens.
                taskMentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: >
                    Array of board task IDs mentioned in the message using {{taskMention:taskId}} tokens. Requires the
                    workspace to have the `boards` capability. In responses, these are populated with task summaries
                    (id, subject, taskNumber, status, boardId, boardName, visibility).
                quotes:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 5
                  default: []
                  description: >
                    Array of message IDs to quote in this message. Quoted messages are referenced using
                    {{quote:messageId}} tokens in content.


                    **Placement Rules:**

                    - **Topic to Topic**: Can quote from topic chats with the same visibility within the same project

                    - **Topic to Submission**: Only reviewer visibility topic chats can be quoted in submissions (same
                    project)

                    - **Submission to Topic/Member**: Can quote submission messages to topic or member chats within the
                    same project

                    - **Member chats**: Messages from member chats cannot be quoted


                    **Permission Requirements:**

                    - User must have permission to view the quoted message

                    - For topic chats: requires canGetCreatorChat or canGetReviewerChat permission based on visibility

                    - For submission chats: requires canGetSubmission permission
                linkPreviews:
                  type: array
                  items:
                    $ref: '#/components/schemas/SharedUrlTitleDescription'
                  maxItems: 5
                  default: []
                  description: Link preview metadata for URLs in the content.
                pageContext:
                  type: object
                  properties:
                    path:
                      type: string
                      maxLength: 2000
                    pageTitle:
                      type: string
                      maxLength: 500
                    visibleAssetIds:
                      type: array
                      items:
                        type: string
                        format: uuid
                      maxItems: 20
                      description: >
                        Assets on screen when the message was sent. Re-read server-side under the caller's visibility
                        tiers; ids they cannot see are dropped.
                    contextItems:
                      type: array
                      items:
                        type: object
                        properties:
                          type:
                            type: string
                            enum:
                              - asset
                              - folder
                              - user
                              - submission
                              - public
                          id:
                            type: string
                            maxLength: 200
                        required:
                          - type
                          - id
                        additionalProperties: false
                      maxItems: 20
                      description: >
                        Context-column pins, as a FALLBACK only. The topic's own stored `contextItems` (see `PATCH
                        /v1/ai/chat/topics/{topicId}`) take precedence whenever it has any — persisting them there is
                        what makes the Context column survive a reload. Identity only; every entry is re-resolved under
                        the caller's live permissions on each turn, and anything they cannot see is silently omitted.
                    recentActions:
                      type: array
                      items:
                        type: string
                        maxLength: 200
                      maxItems: 50
                      description: One-line summaries of the sender's recent actions in this project.
                  additionalProperties: false
                  description: >
                    Snapshot of what the sender is looking at, used only when the target chat is an AI chat (`chatType:
                    'ai'`); every other chat type ignores it. Rendered into that turn's system prompt and never
                    persisted on the message.
              additionalProperties: false
              description: Either `content` or at least one item in `attachments` must be provided.
      responses:
        '200':
          description: >-
            Message created successfully. If upload-shape attachments were included a key called attachmentData will be
            included with asset and signed link data for the newly created assets.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ChatMessage'
                  - type: object
                    properties:
                      attachmentData:
                        $ref: '#/components/schemas/AssetAndSignedLinks'
        '400':
          description: Invalid request parameters (including missing content/attachments and invalid quotes)
        '403':
          description: API token lacks the `chat:write` scope, or the quoted message may not be quoted here
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/asset/{assetId}/{visibility}/new-message:
    post:
      summary: Create a new message in an asset chat, creating the chat if it doesn't exist
      description: >-
        Creates a new message in an asset chat. If the chat doesn't exist for the specified visibility, it will be
        created automatically. Only works with media assets.
      tags:
        - Chats
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset to which the chat belongs.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Visibility level of the chat
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
                  maxLength: 10000
                  description: The message content
                attachments:
                  type: array
                  items:
                    $ref: '#/components/schemas/IdNameOrScratchIdNameUnion'
                  maxItems: 6
                  description: >
                    Attachments for a new message. Two item shapes are accepted, distinguished by the presence of
                    `scratchId`: the upload shape describes new bytes the client will upload via the signed URLs
                    returned in `attachmentData`; the scratch shape references bytes already staged on Scratch (e.g. an
                    AI Revision result) which are promoted to chat-scoped assets when the message lands.
                replyToId:
                  type: string
                  format: uuid
                annotations:
                  type: array
                  items:
                    oneOf:
                      - $ref: '#/components/schemas/DotAnnotationInput'
                      - $ref: '#/components/schemas/FrameCommentAnnotationInput'
                      - $ref: '#/components/schemas/ShapeAnnotationInput'
                      - $ref: '#/components/schemas/TextAnnotationInput'
                      - $ref: '#/components/schemas/PathAnnotationInput'
                      - $ref: '#/components/schemas/Dot3dAnnotationInput'
                    title: AnnotationInput
                  maxItems: 100
                  description: Array of annotations (dots or frame comments)
                mentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: Array of user IDs mentioned in the message using {{mention:userId}} tokens
                assetMentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: >-
                    Array of asset IDs mentioned in the message using {{assetMention:assetId}} tokens. In responses,
                    these are populated with full Asset objects.
                folderMentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: >-
                    Array of folder IDs mentioned in the message using {{folderMention:folderId}} tokens. In responses,
                    these are populated with full Folder objects.
                submissionMentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: Array of submission IDs mentioned in the message using {{submissionMention:submissionId}} tokens.
                publicMentions:
                  type: array
                  items:
                    type: string
                  maxItems: 10
                  default: []
                  description: Array of public release tokens mentioned in the message using {{publicMention:token}} tokens.
                quotes:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 5
                  default: []
                  description: Array of message IDs to quote. Same placement rules apply as for `CreateMessage.quotes`.
                messages:
                  type: number
                  maximum: 100
                  default: 10
                  description: >-
                    Accepted for backwards compatibility. The handler does not forward it, so 10 recent messages are
                    always returned with the chat.
                replies:
                  type: number
                  maximum: 100
                  default: 10
                  description: >-
                    Accepted for backwards compatibility. The handler does not forward it, so 10 replies per message are
                    always returned.
                sort:
                  type: object
                  properties:
                    id:
                      type: number
                      enum:
                        - 1
                        - -1
                    createdAt:
                      type: number
                      enum:
                        - 1
                        - -1
                    updatedAt:
                      type: number
                      enum:
                        - 1
                        - -1
                  additionalProperties: false
                  default:
                    id: -1
                  description: >-
                    Accepted for backwards compatibility. The handler does not forward it, so `{ id: -1 }` is always
                    used.
              additionalProperties: false
        description: Either `content` or at least one attachment must be provided.
      responses:
        '200':
          description: Message created successfully. The chat is re-read after the message lands so `recentMessages` includes it.
          content:
            application/json:
              schema:
                type: object
                properties:
                  chat:
                    $ref: '#/components/schemas/Chat'
                  message:
                    allOf:
                      - $ref: '#/components/schemas/ChatMessage'
                      - type: object
                        properties:
                          attachmentData:
                            allOf:
                              - $ref: '#/components/schemas/AssetAndSignedLinks'
                            description: Present only if upload-shape attachments were included in the request
        '400':
          description: Bad request - Invalid input or missing required content/attachments
        '403':
          description: Forbidden - User does not have permission to create messages in this chat
        '404':
          description: Asset not found, or the asset is not a media asset (`assetInvalidFunctionType`)
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/{chatId}/messages:
    get:
      summary: Get messages for a chat
      description: >
        Returns active messages for a chat with recent replies and populated attachments/mentions. API tokens require
        the `chat:read` scope (`tokenScopeMissing` / 403 otherwise). `createdBefore` / `createdAfter` are only accepted
        with index pagination.
      tags:
        - Chats
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for messages
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Number of messages per page
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination type to use
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include cursor record in results (only for cursor pagination)
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only for cursor pagination)
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include the startAt record in the results (only for cursor pagination)
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number for index pagination
        - name: createdBefore
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            x-conditionally-required: true
          description: >-
            Timestamp or ID indicating the upper bound of result creation time. You may preface an ID with a '+' to make
            the results inclusive of that result Meaning <= instead of just <.
        - name: createdAfter
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            x-conditionally-required: true
          description: >-
            Timestamp or ID indicating the lower bound of result creation time. You may preface an ID with a '+' to make
            the results inclusive of that result Meaning <= instead of just <.
        - name: replies
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Deprecated alias of `replyLimit`. When set to a value other than 10 it takes precedence over `replyLimit`.
        - name: replyLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Number of replies to include per message
        - name: replySort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for replies
        - name: excludeReplies
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Exclude replies from the results
        - name: highlighted
          in: query
          required: false
          schema:
            type: boolean
          description: Filter to highlighted (true) or non-highlighted (false) messages
        - name: contentSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 200
          description: Case-insensitive substring search on message content
        - name: authorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only return messages by this author
        - name: type
          in: query
          required: false
          schema:
            type: string
            enum:
              - user
              - system
          description: Only return user or system messages
        - name: hasAttachments
          in: query
          required: false
          schema:
            type: boolean
          description: Only return messages with (true) or without (false) attachments
        - name: hasAnnotations
          in: query
          required: false
          schema:
            type: boolean
          description: Only return messages with (true) or without (false) annotations
        - name: isConvoMessage
          in: query
          required: false
          schema:
            type: boolean
          description: Only return convo system messages (true) or exclude them (false)
      responses:
        '200':
          description: Successfully retrieved messages
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/IndexPaginatedMessages'
                  - $ref: '#/components/schemas/CursorPaginatedMessages'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/message/{messageId}:
    get:
      summary: Get message by ID
      tags:
        - Chats
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the message
        - name: replies
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Number of recent replies to include with the message
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sort order for the included replies
      responses:
        '200':
          description: Message data retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMessage'
        '404':
          description: Message not found
      security:
        - bearerAuth: []
      x-request-source: joi
    put:
      summary: Revise message by ID
      description: >-
        Revise a message by ID. If annotations are provided, they will replace current annotations not merged. If no
        annotations are provided, current annotations will be preserved. If an empty array is provided, all annotations
        will be removed.
      tags:
        - Chats
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
                  maxLength: 10000
                mentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: Array of user IDs mentioned in the message using {{mention:userId}} tokens
                assetMentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: >-
                    Array of asset IDs mentioned in the message using {{assetMention:assetId}} tokens. In responses,
                    these are populated with full Asset objects.
                folderMentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: >-
                    Array of folder IDs mentioned in the message using {{folderMention:folderId}} tokens. In responses,
                    these are populated with full Folder objects.
                submissionMentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: Array of submission IDs mentioned in the message using {{submissionMention:submissionId}} tokens.
                publicMentions:
                  type: array
                  items:
                    type: string
                  maxItems: 10
                  default: []
                  description: Array of public release tokens mentioned in the message using {{publicMention:token}} tokens.
                taskMentions:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 10
                  default: []
                  description: >
                    Array of board task IDs mentioned in the message using {{taskMention:taskId}} tokens. Requires the
                    workspace to have the `boards` capability. Tasks must be in scope for the chat — see
                    `/chats/{chatId}/mentionable/tasks` for the scoping rules. In responses, these are populated with
                    task summaries (id, subject, taskNumber, status, boardId, boardName, visibility).
                quotes:
                  type: array
                  items:
                    type: string
                    format: uuid
                  maxItems: 5
                  default: []
                  description: Array of message IDs to quote. Same placement rules apply as when creating messages.
                annotations:
                  type: array
                  items:
                    oneOf:
                      - $ref: '#/components/schemas/DotAnnotationInput'
                      - $ref: '#/components/schemas/FrameCommentAnnotationInput'
                      - $ref: '#/components/schemas/ShapeAnnotationInput'
                      - $ref: '#/components/schemas/TextAnnotationInput'
                      - $ref: '#/components/schemas/PathAnnotationInput'
                      - $ref: '#/components/schemas/Dot3dAnnotationInput'
                    title: AnnotationInput
                  maxItems: 100
                linkPreviews:
                  type: array
                  items:
                    $ref: '#/components/schemas/SharedUrlTitleDescription'
                  maxItems: 5
                  default: []
                  description: Link preview metadata for URLs in the content. Replaces the stored previews.
              additionalProperties: false
      responses:
        '200':
          description: Message revised successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMessage'
        '400':
          description: Bad request (invalid input)
        '404':
          description: Message not found
      security:
        - bearerAuth: []
      x-request-source: joi
    delete:
      summary: Delete a message
      description: Marks the message for deletion. It is removed later by the cleanup service.
      tags:
        - Chats
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the message
      responses:
        '200':
          description: Message marked for deletion. Returns the deleted message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMessage'
        '400':
          description: Invalid request parameters
        '404':
          description: Message not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/message/{messageId}/replies:
    get:
      summary: Get replies for a message
      tags:
        - Chats
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for replies
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Number of replies per page
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination type to use
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include cursor record in results (only for cursor pagination)
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only for cursor pagination)
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include the startAt record in the results (only for cursor pagination)
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number for index pagination
        - name: createdBefore
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            default: null
            nullable: true
            x-conditionally-required: true
          description: >-
            Timestamp or ID indicating the upper bound of result creation time. You may preface an ID with a '+' to make
            the results inclusive of that result Meaning <= instead of just <.
        - name: createdAfter
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            default: null
            nullable: true
            x-conditionally-required: true
          description: >-
            Timestamp or ID indicating the lower bound of result creation time. You may preface an ID with a '+' to make
            the results inclusive of that result Meaning <= instead of just <.
      responses:
        '200':
          description: Successfully retrieved replies
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/IndexPaginatedReplies'
                  - $ref: '#/components/schemas/CursorPaginatedReplies'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/message/{messageId}/attachment:
    post:
      summary: Attach a file to a message.
      tags:
        - Chats
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the message to attach a file to
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: integer
                    maximum: 10
                    description: Client-side counter used to match request items with response items.
                    example: 1
                  name:
                    type: string
                    pattern: >-
                      \.(jpg|jpeg|png|gif|tiff|bmp|webp|svg|heif|ico|raw|exr|heic|mp4|avi|mov|wmv|flv|mkv|webm|m4v|mpg|mpeg|rm|vob|3gp|ogv|ts|m2ts|hevc|divx)$
                    description: File name; must end with a supported image or video extension.
                    example: photo.jpg
                  checksum:
                    type: string
                    pattern: ^[a-f0-9]{32,64}$
                    description: MD5 or SHA-256 hash of the file content.
                  sizeInMB:
                    type: number
                    minimum: 0
                    x-exclusiveMinimum: true
                    example: 1.2
                required:
                  - id
                  - name
                  - checksum
                  - sizeInMB
                additionalProperties: false
              maxItems: 6
              description: Files to attach to an existing message. Only image and video extensions are accepted.
      responses:
        '200':
          description: >-
            Attachment assets created and appended to the message. Returns asset and signed upload link data for each
            file.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssetAndSignedLinks'
        '400':
          description: Invalid request parameters
        '404':
          description: Message not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/message/{messageId}/attachment/{assetId}:
    delete:
      summary: Remove an attachment from a message
      description: >
        Removes the asset from the message's attachments and marks the asset for deletion. If the message has no content
        and no attachments left afterwards, the message itself is marked for deletion and the deleted message is
        returned.
      tags:
        - Chats
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the message
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the attached asset to remove
      responses:
        '200':
          description: Attachment removed. Returns the updated (or deleted) message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMessage'
        '404':
          description: Message or asset not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/message/{messageId}/reaction:
    post:
      summary: Create or update a reaction on a chat message
      description: >-
        Creates a new reaction or replaces an existing reaction from the same user on a chat message. Each user can only
        have one reaction per message.
      tags:
        - Chats
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the message to react to
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                emoji:
                  type: string
                  minLength: 1
                  maxLength: 10
                  description: The emoji character(s) to use for the reaction
                  example: 👍
              required:
                - emoji
              additionalProperties: false
      responses:
        '200':
          description: Reaction created or updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMessage'
        '400':
          description: Bad request - Invalid emoji format or missing emoji field
        '403':
          description: Forbidden - User does not have permission to react to this message
        '404':
          description: Message not found
      security:
        - bearerAuth: []
      x-request-source: joi
    delete:
      summary: Remove a user's reaction from a chat message
      description: >-
        Removes the authenticated user's reaction from the specified chat message. Only the user who created the
        reaction can remove it.
      tags:
        - Chats
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the message to remove reaction from
      responses:
        '200':
          description: Reaction removed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMessage'
        '403':
          description: Forbidden - User does not have permission to remove reactions from this message
        '404':
          description: Message not found or user has no reaction on this message
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/message/{messageId}/highlight:
    put:
      summary: Highlight a chat message
      description: >
        Sets `highlighted = true` on the message and records the highlighter. For project and asset topic chats a system
        message is posted in the project chat of the same visibility, a `chatHighlightMessage` notification is sent and
        project members are emailed. Requires the `canHighlightMessage` permission on the chat.
      tags:
        - Chats
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the message
      responses:
        '200':
          description: Message highlighted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMessage'
        '404':
          description: Message not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/message/{messageId}/unhighlight:
    put:
      summary: Remove the highlight from a chat message
      description: >-
        Sets `highlighted = false`, clears the highlighter fields and removes the associated system messages and
        notification. Requires the `canHighlightMessage` permission on the chat.
      tags:
        - Chats
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the message
      responses:
        '200':
          description: Message unhighlighted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMessage'
        '404':
          description: Message not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/mentions:
    get:
      summary: Get messages where the user was mentioned
      tags:
        - Chats
      parameters:
        - name: chatId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter mentions by chat ID
        - name: authorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter mentions by author ID
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Number of results per page
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination type to use
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only for cursor pagination)
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to paginate in reverse order (only for cursor pagination)
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include total counts in response (only for cursor pagination)
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include cursor record in results (only for cursor pagination)
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only for cursor pagination)
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include the startAt record in the results (only for cursor pagination)
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (only for index pagination)
        - name: createdBefore
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            x-conditionally-required: true
          description: Get mentions created before this timestamp (only for index pagination)
      responses:
        '200':
          description: List of messages where user was mentioned
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/IndexPaginatedMessages'
                  - $ref: '#/components/schemas/CursorPaginatedMessages'
        '400':
          $ref: '#/components/responses/BadRequest'
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/workspace/{workspaceId}/project-chats:
    get:
      summary: Get every project topic chat the user can access in a workspace
      description: >
        Returns the creator/reviewer project topic chats across all projects in the workspace that the caller can read,
        with the latest message and message count for each. Access is derived per project from the caller's inherited
        permissions (`canGetCreatorChat` / `canGetReviewerChat`). Requires `canGetWorkspace` on the workspace. Results
        are ordered by most recent activity.
      tags:
        - Chats
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The unique identifier of the workspace
      responses:
        '200':
          description: Accessible project chats
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WorkspaceProjectChat'
      security:
        - bearerAuth: []
      x-request-source: joi
  /config:
    get:
      summary: Get current platform configuration.
      tags:
        - Config
      description: >
        Returns the current platform configuration: validation limits, approved colours, roles and their permissions,
        billable roles, supported / restricted file types, supported media types per asset function, and feature flags
        for convos and link previews. Public — no authentication required.
      responses:
        '200':
          description: Current platform configuration.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Config'
                  - type: object
                    properties:
                      rolesAndPermissions:
                        type: object
                        description: Map of role name to its metadata and permission list.
                        additionalProperties: true
                      billableRoles:
                        type: array
                        items:
                          type: string
                        description: Roles that count toward billing.
                      supportedFileTypes:
                        type: object
                        description: Supported file extensions / mime types, keyed by media type.
                        additionalProperties: true
                      restrictedFileTypes:
                        type: array
                        items:
                          type: string
                        description: File types that are never accepted for upload.
                      supportedMediaTypes:
                        type: object
                        description: Allowed media types per asset function type.
                        properties:
                          attachment:
                            type: array
                            items:
                              type: string
                            example:
                              - image
                              - video
                              - audio
                              - file
                              - 3d
                              - document
                          media:
                            type: array
                            items:
                              type: string
                          avatar:
                            type: array
                            items:
                              type: string
                            example:
                              - image
                          logo:
                            type: array
                            items:
                              type: string
                          icon:
                            type: array
                            items:
                              type: string
                      convos:
                        type: object
                        properties:
                          enabled:
                            type: boolean
                            description: Whether the Convos (voice/video) feature is enabled on this deployment.
                      linkPreview:
                        type: object
                        properties:
                          enabled:
                            type: boolean
                          provider:
                            type: string
                            description: Link preview provider in use.
                          externalUrl:
                            type: string
                            nullable: true
                            description: External preview service URL, when an external provider is configured.
  /convos:
    post:
      summary: Start a new convo
      description: >
        Start a new video or audio convo in a chat. If the chat already has an active convo it is auto-completed

        before the new one is created. Creates a Daily.co room and posts a system message in the chat.

        The starter automatically joins as an active participant and receives a meeting token.


        Gates (in order): convos feature flag (403 `featureDisabled`), active workspace subscription

        (400 `subscriptionNotActive`), the workspace `convos` capability (403 `capabilityNotAvailable`),

        a positive credit balance (400 `creditBalanceInsufficient`) and the same chat permission required to post a
        message

        in the chat (`canStartConvo`).
      tags:
        - Convos
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                chatId:
                  type: string
                  format: uuid
                chatType:
                  type: string
                  enum:
                    - topic
                    - member
                    - submission
                  description: >-
                    Type of chat (topic for projects/assets, member for direct messages, submission for review chats).
                    Stored on the convo as `chat` / `chatMember` / `chatSubmission`.
                convoType:
                  type: string
                  enum:
                    - video
                    - audio
                  description: Type of convo (video call or audio only)
                sendEmailNotification:
                  type: boolean
                  default: false
                  description: When true, chat participants are emailed that a convo started.
                subject:
                  type: string
                  maxLength: 200
                  description: Optional subject, appended to the start system message.
                notes:
                  type: string
                  maxLength: 5000
                  description: Optional notes, appended to the start system message.
              required:
                - chatId
                - chatType
                - convoType
              additionalProperties: false
              example:
                chatId: 0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b
                chatType: topic
                convoType: video
                sendEmailNotification: false
                subject: Design review
      responses:
        '201':
          description: Convo created successfully. The convo is returned with a Daily.co meeting token for the starter.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Convo'
                  - type: object
                    properties:
                      meetingToken:
                        type: string
                        description: Daily.co meeting token for the starter (admin + record privileges).
        '400':
          description: >-
            Validation error, no active subscription (`subscriptionNotActive`) or insufficient credits
            (`creditBalanceInsufficient`)
        '403':
          description: >-
            Convos disabled (`featureDisabled`), workspace lacks the `convos` capability (`capabilityNotAvailable`), or
            the user may not post in the chat
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      parameters: []
      x-request-source: joi
  /convos/{convoId}:
    get:
      summary: Get convo details
      description: >
        Retrieve a convo with populated participants, starter, chat and scope. A convo that is still marked active but
        whose Daily.co room has gone is reaped on read and returned as `completed`. Requires read access to the convo's
        chat.
      tags:
        - Convos
      parameters:
        - name: convoId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Convo ID
      responses:
        '200':
          description: Convo details retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Convo'
        '403':
          description: Convos disabled (`featureDisabled`) or the user does not have access to the chat containing this convo
        '404':
          description: Convo not found
      security:
        - bearerAuth: []
      x-request-source: joi
    delete:
      summary: Cancel/delete a convo
      description: |
        Marks the convo as `cancelled`, clears the chat's active convo and posts a system message.
        Only the user who started the convo can delete it.
      tags:
        - Convos
      parameters:
        - name: convoId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Convo ID
      responses:
        '204':
          description: Convo cancelled. No response body.
        '403':
          description: Convos disabled (`featureDisabled`) or the user is not the convo starter
        '404':
          description: Convo not found
      security:
        - bearerAuth: []
      x-request-source: joi
    patch:
      summary: Update a convo's subject and/or notes
      description: >
        Updates `subject` and/or `notes`. Allowed regardless of convo status so notes can be added after the call ends.
        Only the user who started the convo can update it. At least one field must be provided.
      tags:
        - Convos
      parameters:
        - name: convoId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Convo ID
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                subject:
                  type: string
                  maxLength: 200
                  description: New subject. An empty string clears it.
                notes:
                  type: string
                  maxLength: 5000
                  description: New notes. An empty string clears them.
              additionalProperties: false
              minProperties: 1
              example:
                subject: Design review
                notes: Agreed to ship the new header on Friday.
      responses:
        '200':
          description: Convo updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  convo:
                    $ref: '#/components/schemas/Convo'
        '400':
          description: Validation error or no fields to update
        '403':
          description: Convos disabled (`featureDisabled`) or the user is not the convo starter
        '404':
          description: Convo not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /convos/{convoId}/join:
    put:
      summary: Join an active convo
      description: |
        Join an active convo. User must have access to the chat and the convo must be in active status.
        Returns a Daily.co meeting token for the user to join the room.
        User is added to both activeParticipants and allParticipants arrays.

        **Cross-device handover:** If the user is already an active participant (e.g. joining from a second device),
        the join still succeeds and returns a new meeting token. A `convoHandover` WebSocket notification is sent
        to the user's personal channel (`user/{userId}`) so the old device can close its convo window.
        The user is not duplicated in activeParticipants.
      tags:
        - Convos
      parameters:
        - name: convoId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Convo ID
      responses:
        '200':
          description: Successfully joined the convo (or handed over from another device)
          content:
            application/json:
              schema:
                type: object
                properties:
                  convo:
                    $ref: '#/components/schemas/Convo'
                  token:
                    type: string
                    description: Daily.co meeting token to join the room
        '400':
          description: The Daily.co room has expired; start a new convo
        '403':
          description: Convos disabled (`featureDisabled`) or the user does not have access to this convo
        '404':
          description: Convo not found
        '409':
          description: >-
            Convo is not active (`convoNotActive`). `errorData` carries the current `convo` and `chat` so the UI can
            update.
      security:
        - bearerAuth: []
      x-request-source: joi
  /convos/{convoId}/rejoin:
    put:
      summary: Rejoin a convo (page refresh / reconnect)
      description: |
        Rejoin an active convo after a page refresh or reconnect.
        Only generates a new Daily.co meeting token if the user is already an active participant.
        No notifications are sent. This is a silent reconnect.
      tags:
        - Convos
      parameters:
        - name: convoId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Convo ID
      responses:
        '200':
          description: Successfully rejoined the convo
          content:
            application/json:
              schema:
                type: object
                properties:
                  convo:
                    $ref: '#/components/schemas/Convo'
                  token:
                    type: string
                    description: New Daily.co meeting token
        '400':
          description: Convo is not active (`convoNotActive`) or the Daily.co room has expired
        '403':
          description: Convos disabled (`featureDisabled`) or the user does not have access to this convo
        '404':
          description: Convo not found, or the user is not an active participant
      security:
        - bearerAuth: []
      x-request-source: joi
  /convos/{convoId}/leave:
    put:
      summary: Leave an active convo
      description: |
        Leave an active convo. User must be an active participant.
        User is removed from activeParticipants but remains in allParticipants.
        Creates a system message indicating the user left the convo. If the last active participant leaves,
        the convo is auto-completed and returned with `status: completed`.
      tags:
        - Convos
      parameters:
        - name: convoId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Convo ID
      responses:
        '200':
          description: Successfully left the convo. Returns the updated (possibly auto-completed) convo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Convo'
        '400':
          description: Convo is not active (`convoNotActive`)
        '403':
          description: Convos disabled (`featureDisabled`) or the user is not an active participant in this convo
        '404':
          description: Convo not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /convos/{convoId}/complete:
    put:
      summary: Complete a convo
      description: |
        Mark a convo as completed. Only the user who started the convo can complete it.
        Calculates `durationMinutes`, sets `endedAt` and creates a system message indicating the convo was completed.
      tags:
        - Convos
      parameters:
        - name: convoId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Convo ID
      responses:
        '200':
          description: Convo completed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Convo'
        '400':
          description: Convo is not active (`convoNotActive`)
        '403':
          description: Convos disabled (`featureDisabled`) or the user is not the convo starter
        '404':
          description: Convo not found
        '409':
          description: Convo has already been completed or cancelled. `errorData` carries the current `convo` and `chat`.
      security:
        - bearerAuth: []
      x-request-source: joi
  /convos/scope/{scopeId}:
    get:
      summary: Get convos for a project scope (paginated)
      description: >
        Returns convos across all chats in a project, filtered to the requested chat visibilities. The caller must hold
        the

        chat-read permission for **every** requested visibility (`canGetCreatorChat` / `canGetReviewerChat`).

        Active convos are listed first, then newest first. `search` matches participant names and convo type and is
        applied

        to the current page after the query.
      tags:
        - Convos
      parameters:
        - name: scopeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project ID
        - name: visibility
          in: query
          required: true
          schema:
            type: array
            items:
              type: string
              enum:
                - creator
                - reviewer
            minItems: 1
          style: form
          explode: true
          description: Chat visibilities to include (e.g. `visibility[]=creator&visibility[]=reviewer`)
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - active
              - completed
              - cancelled
          description: Filter convos by status
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: Case-insensitive match against participant names and convo type (current page only)
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: cursor
            enum:
              - cursor
              - index
          description: Pagination type
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Results per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only for cursor pagination)
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Paginate in reverse order (only for cursor pagination)
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include total counts (only for cursor pagination)
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include the cursor record in the results (only for cursor pagination)
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (only for index pagination)
        - name: startAt
          in: query
          required: false
          schema:
            type: integer
            x-conditionally-required: true
          description: Record to start pagination from (only for cursor pagination)
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Include the startAt record in the results (only for cursor pagination)
      responses:
        '200':
          description: >-
            Paginated convos. Cursor pagination returns a `CursorPaginatedResult`, index pagination a `PaginatedResult`;
            both carry the convos in `results`.
          content:
            application/json:
              schema:
                oneOf:
                  - allOf:
                      - $ref: '#/components/schemas/CursorPaginatedResult'
                      - type: object
                        properties:
                          results:
                            type: array
                            items:
                              $ref: '#/components/schemas/Convo'
                  - allOf:
                      - $ref: '#/components/schemas/PaginatedResult'
                      - type: object
                        properties:
                          results:
                            type: array
                            items:
                              $ref: '#/components/schemas/Convo'
        '400':
          description: Validation error (e.g. missing `visibility`)
        '403':
          description: Convos disabled (`featureDisabled`) or the user lacks chat access for one of the requested visibilities
        '404':
          description: Project not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/{chatId}/convos:
    get:
      summary: Get all convos for a chat
      description: >
        Retrieve all convos (active, completed, or cancelled) for a specific chat.

        Results are sorted by creation time in descending order (newest first).

        Optionally filter by status. Requires read access to the chat (`canGetChat`) and an active workspace
        subscription.
      tags:
        - Convos
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Chat ID
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - active
              - completed
              - cancelled
          description: Filter convos by status
      responses:
        '200':
          description: Convos retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Convo'
        '403':
          description: User does not have access to this chat
        '404':
          description: Chat not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /projects/{projectId}/convos:
    get:
      summary: Get all convos for a project (Convos drawer)
      description: |
        Retrieve all convos across all chats within a project.
        Results are sorted by creation time in descending order (newest first).
        Optionally filter by visibility (creator/reviewer) and status.
        This endpoint is used for the "Convos drawer" feature to show all project convos in one place.
        Requires `canGetProject` and an active workspace subscription.
      tags:
        - Convos
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project ID
        - name: visibility
          in: query
          required: false
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Filter convos by chat visibility (creator or reviewer chats)
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - active
              - completed
              - cancelled
          description: Filter convos by status
      responses:
        '200':
          description: Project convos retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Convo'
        '403':
          description: User does not have access to this project
        '404':
          description: Project not found
      security:
        - bearerAuth: []
      x-request-source: joi
  /folders/{folderId}:
    get:
      summary: Get folder details
      description: >
        Returns a folder by ID. When the folder has an icon, the icon asset is populated on `icon`. Requires
        `canGetFolder` on the folder and a live subscription on the owning workspace.
      tags:
        - Folders
      security:
        - bearerAuth: []
      parameters:
        - name: folderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the folder.
      responses:
        '200':
          description: Successfully retrieved folder details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Folder'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Folder not found (`folderNotFound`).
      x-request-source: joi
    put:
      summary: Update folder name or colour
      description: >
        Renames a folder, or updates its colour. The required permission depends on the collection the folder lives in,
        taken from its `ownerResourceType`: a project-owned folder (the creator and reviewer trees) needs
        `canUpdateFolder`; one owned by a submission needs `canAddSubmittionItems`; one owned by a public release needs
        `canUpdatePublicFileSystem`. A creator holds the first but not the latter two, matching the path-scoped routes
        for those collections. Requires a live subscription on the owning workspace.
      tags:
        - Folders
      security:
        - bearerAuth: []
      parameters:
        - name: folderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the folder.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 100
                  description: New folder name (HTML-sanitized). Reserved names (`Review`, `Public`, `Submission`) are rejected.
                color:
                  type: string
                  enum:
                    - '#37474F'
                    - '#FF5722'
                    - '#2962FF'
                    - '#33691E'
                    - '#00796B'
                    - '#455A64'
                    - '#2979FF'
                    - '#827717'
                    - '#7986CB'
                    - '#8E24AA'
                    - '#9575CD'
                    - '#BF360C'
                    - '#01579B'
                    - '#EF6C00'
                    - '#AA00FF'
                    - '#F44336'
                    - '#7C4DFF'
                    - '#E65100'
                    - '#8D6E63'
                    - '#283593'
                    - '#607D8B'
                    - '#009688'
                    - '#FF5252'
                    - '#03A9F4'
                    - '#C2185B'
                    - '#00ACC1'
                    - '#E91E63'
                    - '#5D4037'
                    - '#78909C'
                    - '#1E88E5'
                    - '#D500F9'
                    - '#7E57C2'
                    - '#5C6BC0'
                    - '#558B2F'
                    - '#2E7D32'
                    - '#F50057'
                    - '#004D40'
                    - '#0D47A1'
                    - '#C51162'
                    - '#D50000'
                    - '#6200EA'
                    - '#00BCD4'
                    - '#0277BD'
                  description: >-
                    New folder colour. Must be one of the platform's approved colors (`colors.approvedColors` from `GET
                    /config`).
                  example: '#2962FF'
              additionalProperties: false
      responses:
        '200':
          description: Successfully updated folder.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Folder'
        '400':
          description: >-
            Validation error, or the name is reserved by the system (`reservedFolderName` — `Review`, `Public`,
            `Submission`).
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Folder not found (`folderNotFound`).
      x-request-source: joi
  /folders/{folderId}/assets:
    get:
      summary: Get assets in a folder (not implemented)
      deprecated: true
      description: >
        Deprecated placeholder — this route is not implemented. Every request currently fails with `500 unknownError`;
        if it were reachable it would respond `501 Not Implemented`. Use `GET
        /projects/{projectId}/files/{visibility}/{path}` to list a folder's contents.
      tags:
        - Folders
      security:
        - bearerAuth: []
      parameters:
        - name: folderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the folder.
      responses:
        '500':
          description: Every request currently fails here (`unknownError`); the route is not usable.
        '501':
          description: Not implemented yet.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Not implemented yet
      x-request-source: joi
  /folders/{folderId}/tag:
    put:
      summary: Add a tag to a folder
      description: >
        Adds a project tag to a folder. The tag must belong to the project the folder inherits from, otherwise 404
        `tagNotFound`. Requires `canTagFolder` and a live subscription on the owning workspace.
      tags:
        - Folders
      security:
        - bearerAuth: []
      parameters:
        - name: folderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the folder.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Tag successfully added to the folder.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Folder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Folder not found (`folderNotFound`) or tag not found in the folder's project (`tagNotFound`).
      x-request-source: joi
  /folders/{folderId}/untag:
    put:
      summary: Remove a tag from a folder
      description: |
        Removes a tag from a folder. Requires `canUntagFolder` and a live subscription on the owning workspace.
      tags:
        - Folders
      security:
        - bearerAuth: []
      parameters:
        - name: folderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the folder.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Tag successfully removed from the folder.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Folder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Folder not found (`folderNotFound`).
      x-request-source: joi
  /folders/{folderId}/icon:
    put:
      summary: Update folder icon
      description: >
        Creates an icon asset for the folder and returns multipart upload URLs for it, mirroring the asset upload flow.
        Any existing icon asset is marked `pendingDelete`. Requires `canUpdateFolder` and a live subscription on the
        owning workspace.
      tags:
        - Folders
      security:
        - bearerAuth: []
      parameters:
        - name: folderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the folder.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 100
                  description: File name of the icon image (HTML-sanitized).
                  example: icon.png
                checksum:
                  type: string
                  description: Client-computed checksum of the file.
                  example: d9729feb74992cc3482b350163a1a010
                sizeInMB:
                  type: number
                  maximum: 10
                  description: File size in megabytes.
                  example: 0.25
              required:
                - name
                - checksum
                - sizeInMB
              additionalProperties: false
      responses:
        '200':
          description: Icon asset created; upload the file to the returned signed URLs.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/AssetAndSignedLink'
                  - type: object
                    properties:
                      folder:
                        $ref: '#/components/schemas/Folder'
                      uploadChunkSizeInBytes:
                        type: integer
                        format: int64
                        example: 209715200
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Folder not found (`folderNotFound`).
      x-request-source: joi
  /invites:
    post:
      tags:
        - Invites
      summary: Invite a user to a resource
      description: >-
        Creates an invite and emails the invitee. Requires the right to grant `role` (and every `additionalRoles` entry)
        on the resource — `canCreateInviteToResource`; otherwise 403. If the invitee already has an account the
        `inviteCreate` notification is also pushed to their user channel.
      operationId: inviteUserToResource
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                inviteeEmail:
                  type: string
                  format: email
                resourceType:
                  type: string
                  enum:
                    - workspace
                    - project
                resourceId:
                  type: string
                  format: uuid
                role:
                  type: string
                  enum:
                    - workspaceAdmin
                    - projectAdmin
                    - creator
                    - reviewer
                    - reviewerAdmin
                additionalRoles:
                  type: array
                  items:
                    type: string
                    enum:
                      - workspaceChatMember
                      - workspaceAdmin
                  default: []
                  description: Optional additional workspace-level roles to assign when the invite is accepted.
              required:
                - inviteeEmail
                - resourceType
                - resourceId
                - role
              additionalProperties: false
              example:
                inviteeEmail: new.member@example.com
                resourceType: project
                resourceId: 0192b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b
                role: creator
      responses:
        '201':
          description: Invitation created and sent successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invite'
        '400':
          description: >-
            Validation error, or `inviteToResourceAlreadyExists` — an active invite for this email/resource already
            exists.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters: []
      x-request-source: joi
    get:
      tags:
        - Invites
      summary: Get user's invites
      operationId: getUsersInvites
      security:
        - bearerAuth: []
      parameters:
        - name: inviteType
          in: query
          required: false
          schema:
            type: string
            default: all
            enum:
              - all
              - inviter
              - invitee
          description: |
            Type of invites to retrieve. Possible values:
            - `all`: Retrieve all invites.
            - `inviter`: Retrieve invites sent by the user.
            - `invitee`: Retrieve invites received by the user.
        - name: status
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                enum:
                  - active
                  - canceled
                  - accepted
              - type: array
                items:
                  type: string
                  enum:
                    - active
                    - canceled
                    - accepted
            default: []
          description: |
            Array of invite statuses to filter by. If provided, each element must be one of:
            - `active`
            - `canceled`
            - `accepted`
            Supports duplication (e.g., `['accepted', 'active', 'accepted']`).
      responses:
        '200':
          description: >-
            Successfully retrieved user's invites. `inviteType=all` returns `{ inviter, invitee }`; `inviter` /
            `invitee` return a plain array.
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    description: Shape for `inviteType=all`.
                    properties:
                      inviter:
                        $ref: '#/components/schemas/Invites'
                      invitee:
                        $ref: '#/components/schemas/Invites'
                  - $ref: '#/components/schemas/Invites'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
      x-request-source: joi
  /invites/{inviteId}:
    get:
      tags:
        - Invites
      summary: Get specific invite (public)
      description: >-
        Public route — no authentication. Used by the invite landing page before the invitee has an account. Returns the
        invite with `inviter`, `invitee` and `resource` populated. An unknown id returns an empty body (the record is
        simply not found; no 404 is raised).
      operationId: getInvite
      parameters:
        - name: inviteId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the invite.
      responses:
        '200':
          description: Successfully retrieved invitation details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invite'
        '400':
          $ref: '#/components/responses/BadRequest'
      x-request-source: joi
    delete:
      tags:
        - Invites
      summary: Cancel invite
      description: >-
        Marks the invite `cancelled`. Requires `canDeleteInvite` on the invite's resource. Returns the cancelled invite
        record.
      operationId: cancelInvite
      security:
        - bearerAuth: []
      parameters:
        - name: inviteId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Invitation cancelled successfully. Returns the updated (populated) invite.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invite'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /invites/resource/{resourceId}:
    get:
      summary: Get invites for a specific resource
      tags:
        - Invites
      parameters:
        - name: resourceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the resource (e.g., workspace, project).
        - name: status
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                enum:
                  - active
                  - canceled
                  - accepted
              - type: array
                items:
                  type: string
                  enum:
                    - active
                    - canceled
                    - accepted
            default: []
          description: |
            Array of invite statuses to filter by. If provided, each element must be one of:
            - `active`
            - `canceled`
            - `accepted`
            Supports duplication (e.g., `['accepted', 'active', 'accepted']`).
      security:
        - bearerAuth: []
      description: Requires the `canGetInvite` right on the resource; otherwise 403.
      responses:
        '200':
          description: Successfully retrieved invitations for the resource.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invites'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /invites/accept/{inviteId}:
    post:
      tags:
        - Invites
      summary: Accept an invitation to join a resource
      description: >-
        Joins the caller to the invited resource with the invite's role(s). Requires `canAcceptInvite` (the invite must
        be addressed to the caller's email). Existing members receive a member-join push; a billable role also triggers
        a subscription-update email to the workspace owner.
      operationId: acceptInvite
      security:
        - bearerAuth: []
      parameters:
        - name: inviteId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the invite to accept.
      responses:
        '200':
          description: Invitation accepted successfully. Returns invite and new membership details.
          content:
            application/json:
              schema:
                type: object
                properties:
                  invite:
                    $ref: '#/components/schemas/Invite'
                  membership:
                    $ref: '#/components/schemas/Membership'
        '400':
          description: '`inviteExpired`, `inviteCancelled`, `inviteAccepted` or `inviteInvalid`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                type: inviteExpired
                code: 400
                message: Invite expired.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Permission failure, or `inviteRevoked` — the inviter can no longer grant this role.
        '404':
          $ref: '#/components/responses/InviteNotFound'
      x-request-source: joi
    get:
      deprecated: true
      tags:
        - Invites
      summary: Accept an invitation to join a resource (deprecated alias; use POST)
      description: >-
        Joins the caller to the invited resource with the invite's role(s). Requires `canAcceptInvite` (the invite must
        be addressed to the caller's email). Existing members receive a member-join push; a billable role also triggers
        a subscription-update email to the workspace owner.
      operationId: acceptInvite
      security:
        - bearerAuth: []
      parameters:
        - name: inviteId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the invite to accept.
      responses:
        '200':
          description: Invitation accepted successfully. Returns invite and new membership details.
          content:
            application/json:
              schema:
                type: object
                properties:
                  invite:
                    $ref: '#/components/schemas/Invite'
                  membership:
                    $ref: '#/components/schemas/Membership'
        '400':
          description: '`inviteExpired`, `inviteCancelled`, `inviteAccepted` or `inviteInvalid`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                type: inviteExpired
                code: 400
                message: Invite expired.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Permission failure, or `inviteRevoked` — the inviter can no longer grant this role.
        '404':
          $ref: '#/components/responses/InviteNotFound'
      x-request-source: joi
  /invites/resend/{inviteId}:
    post:
      tags:
        - Invites
      summary: Resend an invite email to the invitee.
      description: >-
        Re-sends the invitation email and refreshes the invite's expiry. Requires `canResendInvite` on the invite.
        Responds with an empty 200 body.
      operationId: resendInvite
      security:
        - bearerAuth: []
      parameters:
        - name: inviteId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the invite to resend.
      responses:
        '200':
          description: Invitation resent successfully (empty body).
        '400':
          description: '`inviteCancelled`, `inviteAccepted` or `inviteInvalid`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/InviteNotFound'
      x-request-source: joi
    get:
      deprecated: true
      tags:
        - Invites
      summary: Resend an invite email to the invitee. (deprecated alias; use POST)
      description: >-
        Re-sends the invitation email and refreshes the invite's expiry. Requires `canResendInvite` on the invite.
        Responds with an empty 200 body.
      operationId: resendInvite
      security:
        - bearerAuth: []
      parameters:
        - name: inviteId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the invite to resend.
      responses:
        '200':
          description: Invitation resent successfully (empty body).
        '400':
          description: '`inviteCancelled`, `inviteAccepted` or `inviteInvalid`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/InviteNotFound'
      x-request-source: joi
  /link-preview:
    post:
      summary: Fetch link previews for one or more URLs.
      tags:
        - Link Preview
      description: |
        Accepts an array of URLs and returns Open Graph / meta tag preview data for each.
        Each preview is signed with an HMAC-SHA256 signature that must be included when
        the preview is submitted with a chat message. Nurama verifies the signature
        when the message is created, to prevent spoofed preview data.

        Duplicate URLs are collapsed and URLs that fail validation, fetching or
        SSRF checks are silently omitted from `previews`, so the array may be
        shorter than the input. Rate limited to 30 requests per minute per IP.
        Requires authentication.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                urls:
                  type: array
                  items:
                    type: string
                    format: uri
                    maxLength: 2048
                    description: Absolute http(s) URL.
                  minItems: 1
                  maxItems: 5
                  description: Array of URLs to generate previews for.
                  example:
                    - https://github.com
                    - https://example.com
              required:
                - urls
              additionalProperties: false
      responses:
        '200':
          description: Successfully fetched link previews.
          content:
            application/json:
              schema:
                type: object
                properties:
                  previews:
                    type: array
                    items:
                      $ref: '#/components/schemas/LinkPreview'
        '400':
          description: Validation error (invalid URLs, empty array, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized - missing or invalid authentication token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters: []
      x-request-source: joi
  /memberships:
    get:
      tags:
        - Membership
      summary: Get logged-in user's memberships
      description: >-
        Returns every membership record of the caller with `resource` (workspace or project) and public `user`
        populated. Ordered workspaces first, newest first.
      operationId: getUsersMemberships
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully retrieved user's memberships.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Memberships'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /memberships/workspace/{workspaceId}:
    get:
      tags:
        - Membership
      summary: Return membership of a workspace. User must have a workspace admin role to retrieve membership records.
      description: >-
        Returns a paginated, per-user membership report covering the workspace and all of its active projects. Requires
        the `canGetWorkspaceMembers` right. `isBillable=true` restricts to billable roles. Only one of the name sorts
        (`firstName`, `lastName`, `displayName`) may be used at a time.
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the workspace to retrieve membership records for.
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 105
            default: null
            description: Optional name search to filter membership records by user names.
        - name: isBillable
          in: query
          required: false
          schema:
            type: boolean
            default: false
            description: Flag to filter memberships based on billable status.
        - name: sort
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/SharedIdFirstNameLastName'
          style: deepObject
          explode: true
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100000
            default: 20
            description: Maximum number of membership records to return (default 20, max `limits.membership.maxMembershipRecords`).
        - name: page
          in: query
          required: false
          schema:
            type: number
            default: 1
            description: Page number for pagination (default 1).
      responses:
        '200':
          description: Successfully retrieved membership records for the specified workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MembershipReportWithTotals'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /memberships/project/{projectId}:
    get:
      tags:
        - Membership
      summary: >-
        Return membership of a project. Membership retrieved are determined by a user's role in the project. For
        example, reviewers can only see reviewers.
      description: >-
        Returns a paginated, per-user membership report for the project (same shape as the workspace report). Requires
        `canGetProjectMembership`; the roles visible depend on the caller's inherited permissions in the project. Only
        one of the name sorts may be used at a time.
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the project to retrieve membership records for.
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 105
            default: null
            description: Optional name search to filter membership records by user names.
        - name: isBillable
          in: query
          required: false
          schema:
            type: boolean
            default: false
            description: Flag to filter memberships based on billable status.
        - name: sort
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/SharedIdFirstNameLastName'
          style: deepObject
          explode: true
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100000
            default: 20
            description: Maximum number of membership records to return (default 20, max `limits.membership.maxMembershipRecords`).
        - name: page
          in: query
          required: false
          schema:
            type: number
            default: 1
            description: Page number for pagination (default 1).
      responses:
        '200':
          description: Successfully retrieved membership records for the specified project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MembershipReportWithTotals'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      x-request-source: joi
  /memberships/{membershipId}:
    delete:
      tags:
        - Membership
      summary: Delete membership record
      description: >-
        Removes a member from a resource. Requires `canDeleteMember`. Owner records cannot be deleted. Responds with an
        empty 200 body.
      security:
        - bearerAuth: []
      parameters:
        - name: membershipId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the membership record to delete.
      responses:
        '200':
          description: Membership deleted successfully (empty body).
        '400':
          description: '`membershipNotFound` or `ownersMayNotBeDelete`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /memberships/leave/{resourceId}:
    delete:
      tags:
        - Membership
      summary: User may delete their own membership record on a resource unless they are a resource owner.
      security:
        - bearerAuth: []
      parameters:
        - name: resourceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the resource (workspace or project) to leave.
      responses:
        '200':
          description: Successfully left the resource (empty body).
        '400':
          description: Invalid id, `membershipNotFound` (caller has no membership on this resource) or `ownersMayNotBeDelete`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
      x-request-source: joi
  /memberships/add-role:
    post:
      tags:
        - Membership
      summary: Add role to membership record
      description: >-
        Requires `canAddMembershipRole` for the role on the membership's resource. Adding an additive role (e.g.
        `reviewerBoardManager`) auto-pairs its parent role. A billable role triggers a subscription-update email.
      operationId: addRoleToMembership
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                membershipId:
                  type: string
                  format: uuid
                  example: 0192b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b
                role:
                  type: string
                  enum:
                    - workspaceAdmin
                    - projectAdmin
                    - creator
                    - reviewer
                    - reviewerAdmin
                    - workspaceChatMember
                    - reviewerBoardManager
                  description: >-
                    One of the manageable roles listed below. Although the schema does not mark it as required, omitting
                    it fails with `400 roleInvalid`.
                  example: creator
              required:
                - membershipId
              additionalProperties: false
      responses:
        '200':
          description: Role successfully added to membership. Returns the updated membership record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Membership'
        '400':
          description: '`roleInvalid` (unknown or omitted role), `ownershipRolesMayNotBeAdded` or `membershipNotFound`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters: []
      x-request-source: joi
  /memberships/remove-role:
    post:
      tags:
        - Membership
      summary: Remove role from membership record
      description: Requires `canRemoveMembershipRole`. Removing a parent role also strips its additive child roles.
      operationId: removeRoleFromMembership
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                membershipId:
                  type: string
                  format: uuid
                  example: 0192b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b
                role:
                  type: string
                  enum:
                    - workspaceAdmin
                    - projectAdmin
                    - creator
                    - reviewer
                    - reviewerAdmin
                    - workspaceChatMember
                    - reviewerBoardManager
                  description: >-
                    One of the manageable roles listed below. Although the schema does not mark it as required, omitting
                    it fails with `400 roleInvalid`.
                  example: creator
              required:
                - membershipId
              additionalProperties: false
      responses:
        '200':
          description: Role successfully removed from membership. Returns the updated membership record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Membership'
        '400':
          description: '`ownersMayNotBeDelete` (owner roles), `membershipNotFound` or `roleNotFound` (role not on the record).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters: []
      x-request-source: joi
  /memberships/mentionable/project/{projectId}/{visibility}:
    get:
      tags:
        - Membership
      summary: Get mentionable users for a project based on visibility
      description: >-
        Requires `canGetProjectMentionableUsers`. Workspace admins of the parent workspace are always included; project
        roles included depend on `visibility`. Users appearing under several memberships are merged with their roles
        concatenated.
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
              - all
          description: >
            Visibility scope. `creator` returns project creators + admins, `reviewer` returns reviewers +
            reviewer-admins + admins, and `all` returns the union (used for task chats whose underlying board is visible
            to both creator and reviewer).
      responses:
        '200':
          description: Successfully retrieved mentionable users.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Mentionable'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      x-request-source: joi
  /memberships/workspace/{workspaceId}/last-seen:
    post:
      tags:
        - Membership
      summary: Get resource-specific last seen timestamps for multiple users in a workspace
      description: >-
        Returns last seen data for specified users within a workspace. Requires the `canGetWorkspaceMembers` right.
        Unknown user ids are silently omitted from `results`; known users with no record get a null `lastSeen`.
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the workspace to get last seen data for.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                userIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                    example: 507f1f77bcf86cd799439011
                  minItems: 1
                  maxItems: 100
                  description: Array of user IDs to retrieve last seen data for (minimum 1, maximum 100).
              required:
                - userIds
              additionalProperties: false
      responses:
        '200':
          description: Successfully retrieved last seen data for users.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        userId:
                          type: string
                          description: The user's ID
                          example: 507f1f77bcf86cd799439011
                        resourceId:
                          type: string
                          description: The workspace ID
                          example: 507f1f77bcf86cd799439012
                        resourceType:
                          type: string
                          enum:
                            - workspace
                          description: The resource type (always 'workspace' for this endpoint)
                        lastSeen:
                          type: string
                          format: date-time
                          nullable: true
                          description: ISO 8601 timestamp of when user last accessed the workspace, or null if never accessed
                          example: '2025-10-24T10:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /memberships/project/{projectId}/last-seen:
    post:
      tags:
        - Membership
      summary: Get resource-specific last seen timestamps for multiple users in a project
      description: >-
        Returns last seen data for specified users within a project. Requires `canGetProjectMembership`. Unknown user
        ids are silently omitted from `results`; known users with no record get a null `lastSeen`.
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the project to get last seen data for.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                userIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                    example: 507f1f77bcf86cd799439011
                  minItems: 1
                  maxItems: 100
                  description: Array of user IDs to retrieve last seen data for (minimum 1, maximum 100).
              required:
                - userIds
              additionalProperties: false
      responses:
        '200':
          description: Successfully retrieved last seen data for users.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        userId:
                          type: string
                          description: The user's ID
                          example: 507f1f77bcf86cd799439011
                        resourceId:
                          type: string
                          description: The project ID
                          example: 507f1f77bcf86cd799439012
                        resourceType:
                          type: string
                          enum:
                            - project
                          description: The resource type (always 'project' for this endpoint)
                        lastSeen:
                          type: string
                          format: date-time
                          nullable: true
                          description: ISO 8601 timestamp of when user last accessed the project, or null if never accessed
                          example: '2025-10-24T10:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /notifications:
    post:
      summary: Get paginated notifications for specified channels and types
      description: Retrieves notifications with support for both cursor and index-based pagination
      tags:
        - Notifications
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                channels:
                  type: array
                  items:
                    example: /project/[projectId]/creator
                types:
                  $ref: '#/components/schemas/Shared5fee7073'
                paginate:
                  type: string
                  default: index
                  enum:
                    - cursor
                    - index
                createdAfter:
                  type: integer
                  x-conditionally-required: true
                  description: Get notifications created after this timestamp (index pagination only)
                createdBefore:
                  type: integer
                  x-conditionally-required: true
                  description: Get notifications created before this timestamp (index pagination only)
                page:
                  type: number
                  x-conditionally-required: true
                  description: Page number (index pagination only)
                startAt:
                  type: string
                  format: uuid
                  x-conditionally-required: true
                  description: ID of the record to start pagination from (cursor pagination only)
                includeStartAtRecord:
                  type: boolean
                  x-conditionally-required: true
                  description: If true, includes the startAt record in the results (cursor pagination only)
                cursor:
                  type: string
                  x-conditionally-required: true
                  description: Cursor for pagination (cursor pagination only)
                paginateReverse:
                  type: boolean
                  x-conditionally-required: true
                  description: If true, paginates in reverse order (cursor pagination only)
                includeCounts:
                  type: boolean
                  x-conditionally-required: true
                  description: If true, includes count information (cursor pagination only)
                includeCursorRecord:
                  type: boolean
                  x-conditionally-required: true
                  description: If true, includes the cursor record as first result (cursor pagination only)
                sort:
                  type: object
                  properties:
                    id:
                      type: number
                      enum:
                        - 1
                        - -1
                    name:
                      type: number
                      enum:
                        - 1
                        - -1
                    createdAt:
                      type: number
                      enum:
                        - 1
                        - -1
                    type:
                      type: number
                      enum:
                        - 1
                        - -1
                  additionalProperties: false
                limit:
                  type: number
                  maximum: 1000
                  default: 100
                  description: Maximum results per page (up to 1000).
              required:
                - channels
              additionalProperties: false
      responses:
        '200':
          description: >-
            Successfully retrieved notifications. Shape depends on `paginate` — `CursorPaginatedNotifications` for
            `cursor`, `IndexPaginatedNotifications` for `index` (default). Internal push-delivery fields are stripped.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/CursorPaginatedNotifications'
                  - $ref: '#/components/schemas/IndexPaginatedNotifications'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters: []
      x-request-source: joi
  /notifications/new:
    post:
      summary: Get new notifications with pagination support
      description: >-
        Retrieves new notifications with support for both cursor and index-based pagination, optionally updating last
        seen records. Note that if a cursor is submitted this is essentially the same as calling the /notifications
        endpoint. Also, if a cursor is submitted you probably want to set the updateLastSeen to false.
      tags:
        - Notifications
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                updateLastSeen:
                  type: boolean
                  default: true
                channels:
                  type: array
                  items:
                    example: /project/[projectId]/creator
                types:
                  $ref: '#/components/schemas/Shared5fee7073'
                limit:
                  type: number
                  maximum: 1000
                  default: 100
                  description: Maximum results per page (up to 1000).
                paginate:
                  type: string
                  default: index
                  enum:
                    - cursor
                    - index
                cursor:
                  type: string
                  x-conditionally-required: true
                  description: Cursor for pagination (cursor pagination only)
                paginateReverse:
                  type: boolean
                  x-conditionally-required: true
                  description: If true, paginates in reverse order (cursor pagination only)
                includeCounts:
                  type: boolean
                  x-conditionally-required: true
                  description: If true, includes count information (cursor pagination only)
                includeCursorRecord:
                  type: boolean
                  x-conditionally-required: true
                  description: If true, includes the cursor record as first result (cursor pagination only)
                startAt:
                  type: string
                  format: uuid
                  x-conditionally-required: true
                  description: ID of the record to start pagination from (cursor pagination only)
                includeStartAtRecord:
                  type: boolean
                  x-conditionally-required: true
                  description: If true, includes the startAt record in the results (cursor pagination only)
                page:
                  type: number
                  x-conditionally-required: true
                  description: Page number (index pagination only)
              required:
                - channels
              additionalProperties: false
      responses:
        '200':
          description: >-
            Successfully retrieved new notifications. `paginate=index` (default) returns
            `IndexPaginatedNewNotifications` (page plus `newNotificationCount`, `createdBefore`, `lastSeen`,
            `updatedLastSeen`); `paginate=cursor` returns a plain `CursorPaginatedNotifications` (no last-seen metadata;
            the last-seen record is still updated when `updateLastSeen` is true).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/CursorPaginatedNotifications'
                  - $ref: '#/components/schemas/IndexPaginatedNewNotifications'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters: []
      x-request-source: joi
  /notifications/count:
    post:
      summary: Get count of new notifications for specified channels and types.
      tags:
        - Notifications
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                channels:
                  type: array
                  items:
                    example: /project/[projectId]/creator
                types:
                  $ref: '#/components/schemas/Shared5fee7073'
              required:
                - channels
              additionalProperties: false
      responses:
        '200':
          description: Successfully retrieved the count of new notifications.
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                    description: Number of new records.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters: []
      x-request-source: joi
  /notifications/count/bulk:
    post:
      summary: Get bulk count of new notifications for specified channels and types
      tags:
        - Notifications
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                channelQueries:
                  type: array
                  items:
                    type: object
                    properties:
                      channels:
                        type: array
                        items: {}
                        description: Array of channel identifiers
                      types:
                        type: array
                        items:
                          type: string
                          enum:
                            - test
                            - notificationUpdate
                            - workspaceCreate
                            - workspaceUpdate
                            - workspaceDelete
                            - workspaceLogoUpdate
                            - projectCreate
                            - projectUpdate
                            - projectDelete
                            - projectLogoUpdate
                            - projectAssetsPublish
                            - projectAssetsUnpublish
                            - projectItemsPublish
                            - projectItemsUnpublish
                            - projectGroupItemPublish
                            - assetNameChange
                            - assetPublish
                            - assetUnpublish
                            - assetTag
                            - assetUntag
                            - publicAssetLinkCreate
                            - assetDelete
                            - assetStatusUpdate
                            - assetPostProcessUpdate
                            - assetFileUpdate
                            - assetGroupUploadComplete
                            - uploadPushSummary
                            - publishPushSummary
                            - submissionPushSummary
                            - memberJoinPush
                            - chatTopicChatCreate
                            - chatMemberChatCreate
                            - chatUpdateSubject
                            - chatMemberUpdate
                            - chatDelete
                            - chatMemberDelete
                            - chatMemberArchive
                            - chatMemberUnarchive
                            - chatCreateMessage
                            - chatReviseMessage
                            - chatRefreshMessage
                            - chatMention
                            - chatDeleteMessage
                            - chatRemoveAttachment
                            - chatHighlightMessage
                            - chatFollow
                            - chatUnfollow
                            - folderCreate
                            - folderUpdate
                            - folderDelete
                            - folderPublish
                            - folderTag
                            - folderUntag
                            - inviteCreate
                            - inviteCancel
                            - inviteAccept
                            - membershipDelete
                            - membershipAddRole
                            - membershipRemoveRole
                            - membershipLeaveResource
                            - workspaceStorageLimitWarning
                            - userEmailVerify
                            - userSelfUpdate
                            - userPublicUpdate
                            - userDeleted
                            - userAvatarUpdate
                            - botCreate
                            - webhookTest
                            - logoUpdate
                            - iconUpdate
                            - subscriptionCreate
                            - subscriptionUpdate
                            - taskAcknowledged
                            - taskStatusUpdate
                            - taskCreate
                            - taskFollow
                            - taskUnfollow
                            - notificationUpdateLastSeen
                            - submissionCreate
                            - submissionUpdate
                            - submissionTag
                            - submissionUntag
                            - aiChatTopicCreate
                            - aiChatTopicUpdate
                            - aiChatMessageCreate
                            - tagCreate
                            - tagUpdate
                            - tagDelete
                            - fileSystemCreate
                            - fileSystemMove
                            - fileSystemCopy
                            - fileSystemDelete
                            - fileSystemPublish
                            - fileSystemUnpublish
                            - publicFileSystemCreate
                            - publicFileSystemUpdate
                            - publicFileSystemDelete
                            - settingsUpdate
                            - convoStart
                            - convoJoin
                            - convoLeave
                            - convoComplete
                            - convoUpdate
                            - convoDelete
                            - convoParticipantJoined
                            - convoParticipantLeft
                            - convoHandover
                            - boardCreate
                            - boardUpdate
                            - boardDelete
                            - boardTaskCreate
                            - boardTaskMove
                            - boardTaskAssign
                            - boardColumnAdd
                            - boardColumnUpdate
                            - boardColumnDelete
                            - boardColumnReorder
                            - boardTaskAdd
                            - boardTaskRemove
                            - boardTaskUpdate
                            - boardTaskLink
                            - boardTaskUnlink
                            - boardTaskRelationAdd
                            - boardTaskRelationRemove
                            - boardFollow
                            - boardUnfollow
                        uniqueItems: true
                        description: >-
                          Optional array of notification types (notification `type` names, e.g. `chatMessageCreate`; see
                          the WebSocket event reference).
                    required:
                      - channels
                    additionalProperties: false
                  minItems: 1
              required:
                - channelQueries
              additionalProperties: false
      responses:
        '200':
          description: >-
            Successfully retrieved bulk counts of new notifications. One entry per `channelQueries` item, in request
            order.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    channels:
                      type: array
                      items:
                        type: string
                      description: Array of channel identifiers
                    types:
                      type: array
                      items:
                        type: string
                      description: Array of notification types as sent in the request (empty array when omitted)
                    count:
                      type: integer
                      description: Number of new notifications for the specified channels and types
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters: []
      x-request-source: joi
  /notifications/last-seen:
    post:
      summary: Get user's last seen records for specified channels and types.
      tags:
        - Notifications
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                channels:
                  type: array
                  items:
                    example: /project/[projectId]/creator
                types:
                  $ref: '#/components/schemas/Shared5fee7073'
              required:
                - channels
              additionalProperties: false
      responses:
        '200':
          description: >-
            Successfully retrieved the last seen record matching exactly these channels (and types). Body is `null` when
            no record exists yet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LastSeen'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
      parameters: []
      x-request-source: joi
    put:
      summary: Update user's last seen records for specified channels and types.
      tags:
        - Notifications
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                channels:
                  type: array
                  items:
                    example: /project/[projectId]/creator
                types:
                  $ref: '#/components/schemas/Shared5fee7073'
              required:
                - channels
              additionalProperties: false
      responses:
        '200':
          description: Successfully updated last seen records.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LastSeen'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
      parameters: []
      x-request-source: joi
  /projects:
    post:
      summary: Create a new project and topic chats with for both reviewers and creators.
      description: >
        Requires `canCreateProject` on the target workspace and an active workspace subscription. Returns
        `maxProjectsPerWorkspace` (400) when the workspace has reached its plan's project cap and `projectNameTaken`
        (400) when a project with the same slug already exists in the workspace.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 100
                  description: Project name. Also used to derive the project slug.
                  example: Spring Campaign
                workspaceId:
                  type: string
                  format: uuid
                  description: Workspace the project is created in. The caller needs `canCreateProject` on it.
              additionalProperties: false
      responses:
        '201':
          description: Project created successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '400':
          description: Validation error, `projectNameTaken` or `maxProjectsPerWorkspace`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters: []
      x-request-source: joi
    get:
      summary: Get projects for the authenticated user
      description: Returns every active project the caller is a member of, with the caller's role attached to each project.
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
          style: deepObject
          explode: true
          description: Sort options (object-style query, e.g. `sort[name]=1`).
      responses:
        '200':
          description: Returns user's projects and their role in each project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Projects'
      x-request-source: joi
  /projects/{projectId}:
    post:
      summary: Create assets and signed upload links for a project.
      description: >
        Creates assets in the project's creator tree and returns signed upload links. Requires `canCreateAsset`. When a
        submitted file's checksum matches an asset already in the project, no bytes are transferred: the platform adds
        another FileSystem reference to the existing asset and reports `status: success` with `referencesExistingAsset:
        true` and no signed URLs.

        In that case the response's `name` is the EXISTING asset's name, because that is what the new reference carries
        — `FileSystem.resourceName` is a denormalized copy of `Asset.name`, so the submitted name never lands. The
        submitted name is returned as `originalName`. Correlate results with requests positionally or by your own upload
        id, not by `name`.

        Returns `uploadRequestExceedsSubscription` (400) when the request would exceed the workspace's subscribed
        storage.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                files:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: integer
                        maximum: 10
                        description: >-
                          This ID is for the requesters use only. It may be any number. It is uses to match request
                          items with response items.
                      name:
                        type: string
                        pattern: ^(?!.*\.(exe|bat|com|msi|vbs|ps1|app|command|tool|sh|bin|run|jar|py|pl|rb)$).*
                        description: Name of the file (must include a valid extension).
                      checksum:
                        type: string
                        pattern: ^[a-f0-9]{32,64}$
                        description: MD5 or SHA-256 hash of the file content
                      sizeInMB:
                        type: number
                        minimum: 0
                        x-exclusiveMinimum: true
                        description: >-
                          File size in MB. This is used only to determine how many signed links to generate for the file
                          and can be rounded to 2 decimals.
                      basePath:
                        type: string
                        description: >-
                          The base path of the file. This is used to create the initial file system path for the file.
                          If undefined the project's root path is used.
                    required:
                      - id
                      - name
                      - checksum
                      - sizeInMB
                    additionalProperties: false
                destinationPath:
                  type: string
                  description: >
                    Creator-tree folder path the files should be added to, relative to the project root (a path already
                    prefixed with `project/{projectId}/creator` is accepted as-is). Omit to upload to the project root.
              additionalProperties: false
      responses:
        '200':
          description: Assets and upload links created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssetAndSignedLinks'
        '400':
          description: Validation error or `uploadRequestExceedsSubscription`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
          description: Unique ID of the project.
      x-request-source: joi
    get:
      summary: Get project details
      description: Requires `canGetProject`. The caller's roles on the project are attached to the response.
      tags:
        - Projects
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully retrieved project details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
      x-request-source: joi
    put:
      summary: Update project details
      description: Requires `canUpdateProject`. A `projectUpdate` notification is emitted only when the name actually changes.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 100
                description:
                  type: string
                  maxLength: 350
                updateSlug:
                  type: boolean
                  default: false
                  description: >-
                    Determines if the slug will be regenerated or not. Meaning you could change the name but keep the
                    old slug as it may be used in static links.
                color:
                  type: string
                  enum:
                    - '#37474F'
                    - '#FF5722'
                    - '#2962FF'
                    - '#33691E'
                    - '#00796B'
                    - '#455A64'
                    - '#2979FF'
                    - '#827717'
                    - '#7986CB'
                    - '#8E24AA'
                    - '#9575CD'
                    - '#BF360C'
                    - '#01579B'
                    - '#EF6C00'
                    - '#AA00FF'
                    - '#F44336'
                    - '#7C4DFF'
                    - '#E65100'
                    - '#8D6E63'
                    - '#283593'
                    - '#607D8B'
                    - '#009688'
                    - '#FF5252'
                    - '#03A9F4'
                    - '#C2185B'
                    - '#00ACC1'
                    - '#E91E63'
                    - '#5D4037'
                    - '#78909C'
                    - '#1E88E5'
                    - '#D500F9'
                    - '#7E57C2'
                    - '#5C6BC0'
                    - '#558B2F'
                    - '#2E7D32'
                    - '#F50057'
                    - '#004D40'
                    - '#0D47A1'
                    - '#C51162'
                    - '#D50000'
                    - '#6200EA'
                    - '#00BCD4'
                    - '#0277BD'
                  description: Hex color code for the project. Must be one of the approved colours.
              additionalProperties: false
      responses:
        '200':
          description: Successfully updated project details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
      x-request-source: joi
    delete:
      summary: Mark a project for deletion.
      description: >
        Requires `canDeleteProject`. Holders are identical to `canUpdateProject` — which is what this route used to
        check — so no caller's access changed; the right that names the operation is now the one that gates it, so
        revoking it from a role actually blocks deletion.
      tags:
        - Projects
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Project successfully marked for deletion. Empty body.
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
      x-request-source: joi
  /projects/{projectId}/search:
    get:
      tags:
        - Projects
      summary: Full-text search across project content.
      description: >
        Searches across assets, chat messages, and tasks within a project using full-text search. The query uses
        web-search syntax: unquoted words must all match, `"quoted text"` matches a phrase, `OR` matches either side and
        a leading `-` excludes a word. Results are ranked by relevance and respect the caller's visibility (creator /
        reviewer) permissions. Each hit includes a populated `record` shaped per the content type's native list view.
        Requires `canGetProject`.
      operationId: searchProject
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 200
          description: >-
            Search term, 1–200 characters (whitespace trimmed). Supports web-search syntax (`"quoted phrase"`, `OR`,
            leading `-` to exclude a word).
        - name: contentTypes
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  enum:
                    - asset
                    - chatMessage
                    - task
              - type: string
                enum:
                  - asset
                  - chatMessage
                  - task
          description: Restrict to one or more content types. Omit to include all.
        - name: dateFrom
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: ISO 8601 date. Only return results created on/after this date.
        - name: dateTo
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: ISO 8601 date. Only return results created on/before this date.
        - name: creatorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Restrict results to content authored by this user.
        - name: sortBy
          in: query
          required: false
          schema:
            type: string
            default: relevance
            enum:
              - relevance
              - dateAsc
              - dateDesc
          description: Sort order.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Paginated, populated search results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResults'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      x-request-source: joi
  /projects/{projectId}/logo:
    post:
      deprecated: true
      tags:
        - Projects
      summary: Create or update project logo. (deprecated alias; use PUT)
      description: >
        Creates the logo asset and returns its signed upload link(s). POST and PUT run the same operation. Requires
        `canUpdateProject`. Returns `uploadRequestExceedsSubscription` (400) when the file would exceed the workspace's
        subscribed storage.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedIdNameChecksum'
      responses:
        '200':
          description: Logo asset and upload links created successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpdateLogoResponse'
        '400':
          description: Validation error or `uploadRequestExceedsSubscription`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
      x-request-source: joi
    put:
      tags:
        - Projects
      summary: Create or update project logo.
      description: Identical to `POST /projects/{projectId}/logo`. Requires `canUpdateProject`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedIdNameChecksum'
      responses:
        '200':
          description: Logo asset and upload links created/updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpdateLogoResponse'
        '400':
          description: Validation error or `uploadRequestExceedsSubscription`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
      x-request-source: joi
  /projects/{projectId}/chat/{visibility}:
    get:
      tags:
        - Projects
      summary: Get project topic chat (creator or reviewer).
      description: >
        Returns the project's topic chat for the given visibility with its most recent messages and replies. Requires
        `canGetCreatorChat` for `creator` and `canGetReviewerChat` for `reviewer`.
      operationId: getProjectChat
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
        - name: updatedBefore
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: >
            Millisecond timestamp. When set, only messages updated before this time are returned (legacy filter; forces
            the creator chat).
        - name: messages
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Number of recent messages to retrieve
        - name: replies
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Number of recent replies to retrieve per message.
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting parameter for chat messages and replies
      responses:
        '200':
          description: Successfully retrieved project chat messages and details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Chat'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      x-request-source: joi
  /projects/{projectId}/feed/{visibility}:
    get:
      summary: Get project feed with assets and chats
      description: >
        Retrieves a paginated feed of assets and their associated chats for a project. Requires `canGetCreatorAssets`
        for `creator` and `canGetReviewerAssets` for `reviewer`. Index pagination is the default; pass `paginate=cursor`
        for cursor pagination. Cursor-only parameters (`cursor`, `paginateReverse`, `includeCounts`,
        `includeCursorRecord`, `startAt`, `includeStartAtRecord`) are rejected with index pagination, and `page`,
        `createdBefore`, `createdAfter` are rejected with cursor pagination.
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination mode. Cursor-only parameters are rejected when `index`; `page` is rejected when `cursor`.
        - name: mediaTypes
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  enum:
                    - image
                    - video
                    - audio
                    - file
                    - 3d
                    - document
                uniqueItems: true
              - type: string
                enum:
                  - image
                  - video
                  - audio
                  - file
                  - 3d
                  - document
            default: []
          description: Filter assets by media type (single value or array).
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              lastMessageAt:
                type: number
                enum:
                  - 1
                  - -1
              publishedOn:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for the feed
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 20
            default: 20
          description: Number of results per page
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (only accepted with `paginate=index`).
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
            default: null
          description: Search assets by name
        - name: createdBefore
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            default: null
            nullable: true
            x-conditionally-required: true
          description: >-
            Timestamp or ID indicating the upper bound of result creation time. You may preface an ID with a '+' to make
            the results inclusive of that result Meaning <= instead of just <.
        - name: createdAfter
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            default: null
            nullable: true
            x-conditionally-required: true
          description: >-
            Timestamp or ID indicating the lower bound of result creation time. You may preface an ID with a '+' to make
            the results inclusive of that result Meaning <= instead of just <.
        - name: chatMessageSort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for embedded chat messages
        - name: chatReplySort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for embedded chat replies
        - name: chatMessageLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Maximum number of embedded chat messages to return per chat
        - name: chatReplyLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Maximum number of embedded replies to return per message
        - name: hideIfNoChatMessages
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: If true, hides assets with no chat messages
        - name: followedChatsOnly
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: If true, filters feed to show only chats the user is following
      responses:
        '200':
          description: Successfully retrieved project feed. Shape depends on `paginate`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Feed'
                  - $ref: '#/components/schemas/CursorPaginatedFeed'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      x-request-source: joi
  /projects/{projectId}/public-audit:
    get:
      summary: Get the public audit report for a project
      description: |
        Returns a paginated list of project assets that are or have been publicly exposed —
        either via a `PublicAssetLink` (direct download link) or via membership in a `Public`
        file system. Requires `canGetPublicAudit` (project/workspace owners and admins).
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: currentlyPublic
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: |
            When `true`, returns only assets currently public — those with an active
            `PublicAssetLink` or membership in an active, non-expired public file system.
            When `false` (default), returns all assets that have ever been public.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number for index pagination.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Number of results per page.
      responses:
        '200':
          description: Successfully retrieved public audit report.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicAuditAssets'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      x-request-source: joi
  /projects/{projectId}/submission:
    post:
      summary: Create a new submission for a project
      description: >
        Note, submissions are a Chat model. Items are taken from the project's reviewer tree (`itemPaths` are relative
        to `project/{projectId}/reviewer`; already-prefixed paths are accepted as-is). Requires `canCreateSubmission`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  default: []
                  description: Reviewer-tree item paths to include in the submission.
                subject:
                  type: string
                  maxLength: 50
                  description: Subject of the submission
                description:
                  type: string
                  maxLength: 5000
                  description: Description of the submission
                version:
                  type: string
                  maxLength: 10
                  description: Version of the submission
                releaseImmediately:
                  type: boolean
                  default: true
                  description: >-
                    When false, the submission is staged as `unreleased` — hidden from reviewers and firing none of the
                    "new submission" side effects (emails, notifications, system messages) until it is released via the
                    `/release` endpoint. Defaults to true (goes live immediately).
              additionalProperties: false
      responses:
        '200':
          description: Created chat submission.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatSubmission'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
      x-request-source: joi
    get:
      summary: Get submissions for a project
      description: >
        Requires `canGetSubmission`. Callers holding `canCreateSubmission` also see staged `unreleased` submissions;
        everyone else sees only `active` ones. Index pagination is the default; pass `paginate=cursor` for cursor
        pagination (cursor-only parameters are rejected with index pagination, and `page`, `createdBefore`,
        `createdAfter` are rejected with cursor pagination).
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination mode. Cursor-only parameters are rejected when `index`; `page` is rejected when `cursor`.
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              subject:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              createdAt: -1
          style: deepObject
          explode: true
          description: Sorting criteria
        - name: search
          in: query
          required: false
          schema:
            type: string
            maxLength: 200
          description: Case-insensitive substring match against submission subject.
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Number of results per page
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (only accepted with `paginate=index`).
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
        - name: createdBefore
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            default: null
            nullable: true
            x-conditionally-required: true
          description: >-
            Timestamp or ID indicating the upper bound of result creation time. You may preface an ID with a '+' to make
            the results inclusive of that result Meaning <= instead of just <.
        - name: createdAfter
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            default: null
            nullable: true
            x-conditionally-required: true
          description: >-
            Timestamp or ID indicating the lower bound of result creation time. You may preface an ID with a '+' to make
            the results inclusive of that result Meaning <= instead of just <.
        - name: chatMessageSort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for embedded chat messages
        - name: chatReplySort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for embedded chat replies
        - name: chatMessageLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Maximum number of embedded chat messages to return per chat
        - name: chatReplyLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Maximum number of embedded replies to return per message
      responses:
        '200':
          description: Successfully retrieved project submissions. Shape depends on `paginate`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Submissions'
                  - $ref: '#/components/schemas/CursorPaginatedSubmissions'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}:
    get:
      summary: Get a specific submission for a project with recent messages.
      description: >
        Note, submissions are a Chat model. Requires `canGetSubmission`. The submission must belong to `projectId`; a
        foreign or unknown id returns `submissionNotFound` (404).
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
        - name: chatMessageSort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for embedded chat messages
        - name: chatReplySort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for embedded chat replies
        - name: chatMessageLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Maximum number of embedded chat messages to return per chat
        - name: chatReplyLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Maximum number of embedded replies to return per message
      responses:
        '200':
          description: Successfully retrieved chat submission.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ChatSubmission'
                  - type: object
                    properties:
                      queryAt:
                        type: integer
                        format: int64
                        description: Millisecond timestamp of when the query ran.
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/SubmissionNotFound'
      x-request-source: joi
    put:
      summary: Update a submission
      description: Requires `canUpdateSubmission`. The submission must belong to `projectId`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                subject:
                  type: string
                  maxLength: 50
                  description: Updated subject of the submission
                description:
                  type: string
                  maxLength: 5000
                  description: Updated description of the submission
                version:
                  type: string
                  maxLength: 10
                  description: Updated version of the submission
              additionalProperties: false
      responses:
        '200':
          description: Submission updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatSubmission'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/SubmissionNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}/tag:
    put:
      summary: Add a tag to a submission
      description: Requires `canTagSubmission`. The submission must belong to `projectId`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
                  description: The ID of the tag to add
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Tag added successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatSubmission'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Submission or tag not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
    post:
      deprecated: true
      summary: Add a tag to a submission (deprecated alias; use PUT)
      description: Requires `canTagSubmission`. The submission must belong to `projectId`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
                  description: The ID of the tag to add
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Tag added successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatSubmission'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Submission or tag not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}/untag:
    put:
      summary: Remove a tag from a submission
      description: Requires `canUntagSubmission`. The submission must belong to `projectId`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
                  description: The ID of the tag to remove
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Tag removed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatSubmission'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Submission or tag not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
    post:
      deprecated: true
      summary: Remove a tag from a submission (deprecated alias; use PUT)
      description: Requires `canUntagSubmission`. The submission must belong to `projectId`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
                  description: The ID of the tag to remove
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Tag removed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatSubmission'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Submission or tag not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}/release:
    post:
      summary: Release a staged (unreleased) submission
      description: >-
        Flips a submission's status from `unreleased` to `active`, making it visible to reviewers and firing the
        deferred "new submission" side effects (project-member email, notification, push summary, and system messages in
        the creator + reviewer project chats). Requires `canUpdateSubmission`. Returns `submissionNotUnreleased` (400)
        if the submission isn't in the `unreleased` state.
      tags:
        - Projects
      security:
        - bearerAuth: []
      responses:
        '200':
          description: The released chat submission.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatSubmission'
        '400':
          description: Bad request (e.g. `submissionNotUnreleased`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/SubmissionNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}/release-update:
    post:
      summary: Re-release an already-released submission's side effects
      description: >-
        The "Release Update" action for a live (`active`) submission. Re-fires the reviewer-facing side effects — the
        submission-update email template, a `submissionUpdate` notification, and system messages in the creator +
        reviewer project chats — to announce that a released submission has changed. Does NOT change the submission's
        status. Requires `canUpdateSubmission`. Returns `submissionNotFound` if no active submission matches.
      tags:
        - Projects
      security:
        - bearerAuth: []
      responses:
        '200':
          description: The submission (status unchanged).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatSubmission'
        '404':
          $ref: '#/components/responses/SubmissionNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}/files:
    get:
      summary: Get file system items at the root of a submission
      description: >
        Lists the items directly under `submission/{submissionId}`. Requires `canGetSubmission`; the submission must
        belong to `projectId`. Each item carries its populated `resource` and, for assets, the reviewer chat. Index
        pagination is the default; pass `paginate=cursor` for cursor pagination (cursor-only parameters are rejected
        with index pagination and `page` is rejected with cursor pagination). `resourceIds` and `resourceSlugs` may not
        both be set (`invalidItemSearchParams`, 400).
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: '0'
          in: path
          required: true
          schema:
            type: string
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
        - name: path
          in: path
          required: true
          schema:
            type: string
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination mode. Cursor-only parameters are rejected when `index`; `page` is rejected when `cursor`.
        - name: recursiveSearch
          in: query
          required: false
          schema:
            type: boolean
          description: If true, includes items in every sub-folder beneath the path instead of only direct children.
        - name: resourceIds
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by specific resource IDs. May not be combined with `resourceSlugs`.
        - name: resourceSlugs
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  pattern: ^[a-z0-9-]+$
              - type: string
                pattern: ^[a-z0-9-]+$
            default: []
          description: Filter by specific resource slugs. May not be combined with `resourceIds`.
        - name: resourceType
          in: query
          required: false
          schema:
            type: string
            enum:
              - asset
              - folder
          description: Filter by resource type
        - name: resourceTags
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by tag IDs
        - name: creatorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by the resource creator's user ID
        - name: mediaTypes
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  enum:
                    - image
                    - video
                    - folder
                    - file
              - type: string
                enum:
                  - image
                  - video
                  - folder
                  - file
            default: []
          description: Filter by media types (single value or array)
        - name: resourceStatus
          in: query
          required: false
          schema:
            type: string
            default: active
            enum:
              - active
              - pendingDelete
          description: Filter by resource status
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
          description: Partial, case-insensitive match on the resource name
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              mediaType:
                type: number
                enum:
                  - 1
                  - -1
              status:
                type: number
                enum:
                  - 1
                  - -1
              sizeInBytes:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: >-
            Sorting criteria. Keys are mapped onto the FileSystem entry's resource fields (`name` → `resourceName`,
            `createdAt` → `resourceCreatedAt`, ...).
        - name: limit
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            maximum: 100
            default: 10
          description: Number of results per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
        - name: page
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            x-conditionally-required: true
          description: Page number (only accepted with `paginate=index`)
      responses:
        '200':
          description: Successfully retrieved submission files (includes `queryAt`).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/FileSystemItems'
                  - $ref: '#/components/schemas/CursorPaginatedFileSystemItems'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/SubmissionNotFound'
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}/files/{path}:
    get:
      summary: Get file system items at a path within a submission
      description: >
        Same as the root listing, scoped to a folder path inside the submission. Path segments may be folder slugs or
        IDs (IDs are resolved to slugs; an unknown segment returns `fileSystemPathNotFound`, 404). Requires
        `canGetSubmission`; the submission must belong to `projectId`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: '0'
          in: path
          required: true
          schema:
            type: string
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
        - name: path
          in: path
          required: true
          schema:
            type: string
          description: >-
            Folder path within the tree. May contain several `/`-separated segments; each segment is a folder slug or
            ID.
          example: Renders/2024
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination mode. Cursor-only parameters are rejected when `index`; `page` is rejected when `cursor`.
        - name: recursiveSearch
          in: query
          required: false
          schema:
            type: boolean
          description: If true, includes items in every sub-folder beneath the path instead of only direct children.
        - name: resourceIds
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by specific resource IDs. May not be combined with `resourceSlugs`.
        - name: resourceSlugs
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  pattern: ^[a-z0-9-]+$
              - type: string
                pattern: ^[a-z0-9-]+$
            default: []
          description: Filter by specific resource slugs. May not be combined with `resourceIds`.
        - name: resourceType
          in: query
          required: false
          schema:
            type: string
            enum:
              - asset
              - folder
          description: Filter by resource type
        - name: resourceTags
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by tag IDs
        - name: creatorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by the resource creator's user ID
        - name: mediaTypes
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  enum:
                    - image
                    - video
                    - folder
                    - file
              - type: string
                enum:
                  - image
                  - video
                  - folder
                  - file
            default: []
          description: Filter by media types (single value or array)
        - name: resourceStatus
          in: query
          required: false
          schema:
            type: string
            default: active
            enum:
              - active
              - pendingDelete
          description: Filter by resource status
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
          description: Partial, case-insensitive match on the resource name
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              mediaType:
                type: number
                enum:
                  - 1
                  - -1
              status:
                type: number
                enum:
                  - 1
                  - -1
              sizeInBytes:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: >-
            Sorting criteria. Keys are mapped onto the FileSystem entry's resource fields (`name` → `resourceName`,
            `createdAt` → `resourceCreatedAt`, ...).
        - name: limit
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            maximum: 100
            default: 10
          description: Number of results per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
        - name: page
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            x-conditionally-required: true
          description: Page number (only accepted with `paginate=index`)
      responses:
        '200':
          description: Successfully retrieved submission files at path (includes `queryAt`).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/FileSystemItems'
                  - $ref: '#/components/schemas/CursorPaginatedFileSystemItems'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Submission not found, or a path segment does not exist (`fileSystemPathNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}/files/create-folder:
    post:
      summary: Create a new folder within a submission
      description: >
        Requires `canCreateSubmissionFolder`; the submission must belong to `projectId`. Reserved names return
        `reservedFolderName` (400), a `basePath` inside a virtual folder returns `protectedBasePath` (400), and a
        `basePath` that does not exist in the submission returns `basePathNotFound` (404). Recomputes the submission's
        inventory.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedNameColorBasePath'
      responses:
        '200':
          description: Folder created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Folder'
        '400':
          description: Validation error, `reservedFolderName` or `protectedBasePath`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Submission not found or `basePathNotFound`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}/files/move:
    put:
      summary: Move items within a submission
      description: >
        Paths are relative to `submission/{submissionId}`. Items already in the destination folder are skipped (`count`
        may be 0). Requires `canMoveSubmissionItems`; the submission must belong to `projectId`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Array of item paths to move
                  example:
                    - Renders/final.png
                    - Renders/alt
                destinationPath:
                  type: string
                  minLength: 1
                  description: Destination folder path for the items
                  example: Approved
              required:
                - itemPaths
                - destinationPath
              additionalProperties: false
      responses:
        '200':
          description: Items moved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileOperationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Submission not found, or a source/destination path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}/files/copy:
    put:
      summary: Copy items within a submission
      description: >
        Paths are relative to `submission/{submissionId}`. Requires `canCopySubmissionItems`; the submission must belong
        to `projectId`. Recomputes the submission's inventory.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Array of item paths to copy
                  example:
                    - Renders/final.png
                destinationPath:
                  type: string
                  minLength: 1
                  description: Destination folder path for the items
                  example: Approved
              required:
                - itemPaths
                - destinationPath
              additionalProperties: false
      responses:
        '200':
          description: Items copied successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileOperationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Submission not found, or a source/destination path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}/files/delete:
    put:
      summary: Delete items within a submission
      description: >
        Paths are relative to `submission/{submissionId}`. Requires `canDeleteSubmissionItems`; the submission must
        belong to `projectId`. Recomputes the submission's inventory.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Array of item paths to delete
                  example:
                    - Renders/old.png
              required:
                - itemPaths
              additionalProperties: false
      responses:
        '200':
          description: Items deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileOperationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Submission not found, or an item path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/submission/{submissionId}/add:
    post:
      summary: Add reviewer-tree items to an existing submission
      description: >
        Copies items from the project's reviewer tree into the submission. `itemPaths` are relative to
        `project/{projectId}/reviewer` and `destinationPath` is relative to `submission/{submissionId}` (omit it to add
        at the submission root; a destination folder that does not exist returns 404). Marks the copied assets as
        belonging to the submission and recomputes its inventory. Requires `canAddSubmittionItems`; the submission must
        belong to `projectId`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Reviewer-tree item paths to add.
                  example:
                    - Renders/final.png
                    - Renders/alt
                destinationPath:
                  type: string
                  description: Folder inside the submission to add the items to. Defaults to the submission root.
                  example: Round 2
              required:
                - itemPaths
              additionalProperties: false
      responses:
        '200':
          description: Items added successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileOperationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Submission not found, or a source/destination path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the submission. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/assets/{visibility}:
    get:
      summary: Get assets for a project with specified visibility
      description: >
        Retrieves assets for a project filtered by visibility level, optionally with their chats, with support for both
        cursor and index-based pagination. Requires `canGetCreatorAssets` for `creator` and `canGetReviewerAssets` for
        `reviewer`. Cursor-only parameters are rejected with index pagination, and `page`, `createdBefore`,
        `createdAfter` are rejected with cursor pagination.
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination mode. Cursor-only parameters are rejected when `index`; `page` is rejected when `cursor`.
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
            default: null
          description: Search assets by name
        - name: mediaTypes
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  enum:
                    - image
                    - video
                    - audio
                    - file
                    - 3d
                    - document
                uniqueItems: true
              - type: string
                enum:
                  - image
                  - video
                  - audio
                  - file
                  - 3d
                  - document
            default: []
          description: Filter assets by media type (single value or array).
        - name: createdBefore
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            default: null
            nullable: true
            x-conditionally-required: true
          description: >-
            Timestamp or ID indicating the upper bound of result creation time. You may preface an ID with a '+' to make
            the results inclusive of that result Meaning <= instead of just <.
        - name: createdAfter
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            default: null
            nullable: true
            x-conditionally-required: true
          description: >-
            Timestamp or ID indicating the lower bound of result creation time. You may preface an ID with a '+' to make
            the results inclusive of that result Meaning <= instead of just <.
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              publishedOn:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
          style: deepObject
          explode: true
          description: Sorting criteria for assets
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 20
            default: 20
          description: Number of results per page
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (only accepted with `paginate=index`).
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
        - name: includeChats
          in: query
          required: false
          schema:
            type: boolean
            default: true
          description: Include associated chats in the response
        - name: chatMessageSort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for embedded chat messages
        - name: chatReplySort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for embedded chat replies
        - name: chatMessageLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Maximum number of embedded chat messages to return per chat
        - name: chatReplyLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Maximum number of embedded replies to return per message
        - name: hideIfNoChatMessages
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: If true, hides assets with no chat messages
        - name: folderId
          in: query
          required: false
          schema:
            type: string
            format: uuid
            default: null
            nullable: true
          description: Restrict to assets in this folder.
        - name: ignoreFolder
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: If true, ignores folder placement when listing assets.
      responses:
        '200':
          description: >-
            Successfully retrieved assets. Shape depends on `paginate`; `chats.{visibility}` is populated only when
            `includeChats` is true.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Feed'
                  - $ref: '#/components/schemas/CursorPaginatedFeed'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      x-request-source: joi
  /projects/{projectId}/files/{visibility}/create-folder:
    post:
      summary: Create a new folder in a project
      description: >
        Creates a folder in the creator or reviewer tree. The required permission depends on `visibility`, as it does
        for move and copy: `creator` needs `canCreateFolder`; `reviewer` needs `canCreateReviewerFolder`, which only the
        workspace and project admin tiers hold. A creator can publish into the reviewer tree but not create folders
        there directly, matching the fact that they hold no reviewer move or copy right. Reserved names (`Review`,
        `Public`, `Submission`) are rejected (`reservedFolderName`, 400), as are base paths inside a virtual folder
        (`protectedBasePath`, 400). A `basePath` that does not exist returns `basePathNotFound` (404).
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedNameColorBasePath'
      responses:
        '200':
          description: Folder created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Folder'
        '400':
          description: Validation error, `reservedFolderName` or `protectedBasePath`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Project not found or `basePathNotFound`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
      x-request-source: joi
  /projects/{projectId}/files/{visibility}/move:
    put:
      summary: Move items to a new path within a project
      description: >
        Paths are relative to `project/{projectId}/{visibility}`. Requires `canMoveCreatorItems` for `creator` and
        `canMoveReviewerItems` for `reviewer`. Items already in the destination folder are skipped (`count` may be 0).


        A destination inside a virtual folder is a copy, not a move, and costs the right of the matching add route:
        `Public/{token}/...` requires `canAddPubicFileSystemItems`, `Submission/{id}/...` requires
        `canAddSubmittionItems`, and dropping onto `Review/...` publishes the items (requires `canPublishAsset`). Those
        responses carry `virtualDestination: true` (and `published: true` plus `errors` for `Review/`).
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Array of item paths to move
                  example:
                    - Renders/final.png
                    - Renders/alt
                destinationPath:
                  type: string
                  minLength: 1
                  description: Destination folder path for the items
                  example: Approved
              required:
                - itemPaths
                - destinationPath
              additionalProperties: false
      responses:
        '200':
          description: Items moved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileOperationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Project not found, or a source/destination path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
      x-request-source: joi
  /projects/{projectId}/files/{visibility}/copy:
    put:
      summary: Copy items to a new path within a project
      description: >
        Paths are relative to `project/{projectId}/{visibility}`. Requires `canCopyCreatorItems` for `creator` and
        `canCopyReviewerItems` for `reviewer`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Array of item paths to copy
                  example:
                    - Renders/final.png
                destinationPath:
                  type: string
                  minLength: 1
                  description: Destination folder path for the items
                  example: Approved
              required:
                - itemPaths
                - destinationPath
              additionalProperties: false
      responses:
        '200':
          description: Items copied successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileOperationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Project not found, or a source/destination path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
      x-request-source: joi
  /projects/{projectId}/files/{visibility}/delete-preview:
    post:
      summary: Preview what deleting items at these paths would reach
      description: >
        Answers what a delete would remove beyond the rows named, without deleting anything. Takes the same body the
        delete takes and resolves it through the same cascade, so the preview cannot disagree with the outcome.


        Only SECONDARY references come back — the reviewer, submission and public-release copies that would go with the
        selection — not the rows the caller listed.


        Costs the same right as the delete itself (`canDeleteCreatorItems` / `canDeleteReviewerItems`). A cheaper
        preview would hand out the labels of submissions and public releases the caller cannot otherwise see.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Array of item paths to delete
                  example:
                    - Renders/old.png
              required:
                - itemPaths
              additionalProperties: false
      responses:
        '200':
          description: The impact of the proposed delete
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteImpact'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Project not found, or an item path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
      x-request-source: joi
  /projects/{projectId}/files/{visibility}/delete:
    put:
      summary: Delete items at specified paths within a project
      description: >
        Paths are relative to `project/{projectId}/{visibility}`. Requires `canDeleteCreatorItems` for `creator` and
        `canDeleteReviewerItems` for `reviewer`. Use `delete-preview` first to learn which secondary references the
        cascade will remove.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Array of item paths to delete
                  example:
                    - Renders/old.png
              required:
                - itemPaths
              additionalProperties: false
      responses:
        '200':
          description: Items deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileOperationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Project not found, or an item path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
      x-request-source: joi
    delete:
      deprecated: true
      summary: Delete items at specified paths within a project (deprecated alias; use PUT)
      description: >
        Paths are relative to `project/{projectId}/{visibility}`. Requires `canDeleteCreatorItems` for `creator` and
        `canDeleteReviewerItems` for `reviewer`. Use `delete-preview` first to learn which secondary references the
        cascade will remove.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Array of item paths to delete
                  example:
                    - Renders/old.png
              required:
                - itemPaths
              additionalProperties: false
      responses:
        '200':
          description: Items deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileOperationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Project not found, or an item path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
      x-request-source: joi
  /projects/{projectId}/files/{visibility}:
    get:
      summary: Get items at the root path of a project
      description: >
        Lists the items directly under `project/{projectId}/{visibility}`. Requires `canGetCreatorItems` for `creator`
        and `canGetReviewerItems` for `reviewer`. On the first page of an unfiltered listing the synthesized virtual
        roots (`Review`, `Public`, `Submission`) the caller may read are prepended to `results`. Index pagination is the
        default; pass `paginate=cursor` for cursor pagination (cursor-only parameters are rejected with index pagination
        and `page` is rejected with cursor pagination). `resourceIds` and `resourceSlugs` may not both be set
        (`invalidItemSearchParams`, 400).
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: '0'
          in: path
          required: true
          schema:
            type: string
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
        - name: path
          in: path
          required: true
          schema:
            type: string
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination mode. Cursor-only parameters are rejected when `index`; `page` is rejected when `cursor`.
        - name: recursiveSearch
          in: query
          required: false
          schema:
            type: boolean
          description: If true, includes items in every sub-folder beneath the path instead of only direct children.
        - name: resourceIds
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by specific resource IDs. May not be combined with `resourceSlugs`.
        - name: resourceSlugs
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  pattern: ^[a-z0-9-]+$
              - type: string
                pattern: ^[a-z0-9-]+$
            default: []
          description: Filter by specific resource slugs. May not be combined with `resourceIds`.
        - name: resourceType
          in: query
          required: false
          schema:
            type: string
            enum:
              - asset
              - folder
          description: Filter by resource type
        - name: resourceTags
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by tag IDs
        - name: creatorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by the resource creator's user ID
        - name: mediaTypes
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  enum:
                    - image
                    - video
                    - audio
                    - folder
                    - file
              - type: string
                enum:
                  - image
                  - video
                  - audio
                  - folder
                  - file
            default: []
          description: Filter by media types (single value or array)
        - name: resourceStatus
          in: query
          required: false
          schema:
            type: string
            default: active
            enum:
              - active
              - pendingDelete
          description: Filter by resource status
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
          description: Partial, case-insensitive match on the resource name
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              mediaType:
                type: number
                enum:
                  - 1
                  - -1
              status:
                type: number
                enum:
                  - 1
                  - -1
              sizeInBytes:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: >-
            Sorting criteria. Keys are mapped onto the FileSystem entry's resource fields (`name` → `resourceName`,
            `createdAt` → `resourceCreatedAt`, ...).
        - name: limit
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            maximum: 100
            default: 10
          description: Number of results per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
        - name: page
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            x-conditionally-required: true
          description: Page number (only accepted with `paginate=index`)
      responses:
        '200':
          description: Successfully retrieved items at path. Shape depends on `paginate`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/FileSystemItems'
                  - $ref: '#/components/schemas/CursorPaginatedFileSystemItems'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      x-request-source: joi
    post:
      summary: Get items at the root path of a project (filters in the body)
      description: >
        Identical to the GET listing, but the query options are sent as a JSON body instead of query-string parameters —
        useful when the filter set (for example a long `resourceIds` list) would not fit in a URL. Same permissions and
        response shapes as the GET.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedPaginateRecursiveSearchResourceIds'
      responses:
        '200':
          description: Successfully retrieved items at path. Shape depends on `paginate`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/FileSystemItems'
                  - $ref: '#/components/schemas/CursorPaginatedFileSystemItems'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: '0'
          in: path
          required: true
          schema:
            type: string
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
        - name: path
          in: path
          required: true
          schema:
            type: string
      x-request-source: joi
  /projects/{projectId}/files/{visibility}/{path}:
    get:
      summary: Get items at a specific path within a project
      description: >
        Same as the root listing, scoped to a folder path. Path segments may be folder slugs or IDs (IDs are resolved to
        slugs; an unknown segment returns `fileSystemPathNotFound`, 404). Virtual paths are supported: `Public` and
        `Submission` list one synthesized sub-folder per release / submission the caller may read
        (`canGetPublicFileSystem` / `canGetSubmission`), and `Public/{token}/...`, `Submission/{id}/...` and
        `Review/...` are translated to the underlying tree. Requires `canGetCreatorItems` for `creator` and
        `canGetReviewerItems` for `reviewer`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: '0'
          in: path
          required: true
          schema:
            type: string
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
        - name: path
          in: path
          required: true
          schema:
            type: string
          description: >-
            Folder path within the tree. May contain several `/`-separated segments; each segment is a folder slug or
            ID.
          example: Renders/2024
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination mode. Cursor-only parameters are rejected when `index`; `page` is rejected when `cursor`.
        - name: recursiveSearch
          in: query
          required: false
          schema:
            type: boolean
          description: If true, includes items in every sub-folder beneath the path instead of only direct children.
        - name: resourceIds
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by specific resource IDs. May not be combined with `resourceSlugs`.
        - name: resourceSlugs
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  pattern: ^[a-z0-9-]+$
              - type: string
                pattern: ^[a-z0-9-]+$
            default: []
          description: Filter by specific resource slugs. May not be combined with `resourceIds`.
        - name: resourceType
          in: query
          required: false
          schema:
            type: string
            enum:
              - asset
              - folder
          description: Filter by resource type
        - name: resourceTags
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by tag IDs
        - name: creatorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by the resource creator's user ID
        - name: mediaTypes
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  enum:
                    - image
                    - video
                    - audio
                    - folder
                    - file
              - type: string
                enum:
                  - image
                  - video
                  - audio
                  - folder
                  - file
            default: []
          description: Filter by media types (single value or array)
        - name: resourceStatus
          in: query
          required: false
          schema:
            type: string
            default: active
            enum:
              - active
              - pendingDelete
          description: Filter by resource status
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
          description: Partial, case-insensitive match on the resource name
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              mediaType:
                type: number
                enum:
                  - 1
                  - -1
              status:
                type: number
                enum:
                  - 1
                  - -1
              sizeInBytes:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: >-
            Sorting criteria. Keys are mapped onto the FileSystem entry's resource fields (`name` → `resourceName`,
            `createdAt` → `resourceCreatedAt`, ...).
        - name: limit
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            maximum: 100
            default: 10
          description: Number of results per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
        - name: page
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            x-conditionally-required: true
          description: Page number (only accepted with `paginate=index`)
      responses:
        '200':
          description: Successfully retrieved items at path. Shape depends on `paginate`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/FileSystemItems'
                  - $ref: '#/components/schemas/CursorPaginatedFileSystemItems'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Project not found, or a path segment does not exist (`fileSystemPathNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
    post:
      summary: Get items at a specific path within a project (filters in the body)
      description: >
        Identical to the GET listing at a path, but the query options are sent as a JSON body instead of query-string
        parameters. Same permissions and response shapes as the GET.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedPaginateRecursiveSearchResourceIds'
      responses:
        '200':
          description: Successfully retrieved items at path. Shape depends on `paginate`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/FileSystemItems'
                  - $ref: '#/components/schemas/CursorPaginatedFileSystemItems'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Project not found, or a path segment does not exist (`fileSystemPathNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: '0'
          in: path
          required: true
          schema:
            type: string
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: visibility
          in: path
          required: true
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: Which tree to operate on — the creator tree or the reviewer tree.
        - name: path
          in: path
          required: true
          schema:
            type: string
          description: >-
            Folder path within the tree. May contain several `/`-separated segments; each segment is a folder slug or
            ID.
          example: Renders/2024
      x-request-source: joi
  /projects/{projectId}/publish:
    post:
      summary: Publish assets
      description: >
        Bulk publish an array of file-system items (assets or folders) into the project's reviewer tree. The caller must
        hold `canPublishItem` on every listed resource; otherwise 403 with the offending ids in `errorData`. Fires
        per-item and project-level publish notifications, system messages in the creator and reviewer chats and, when
        `sendEmailNotification` is true, the published-items email.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                resourceIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: Array of resource IDs (assets or folders) to publish
                sendEmailNotification:
                  type: boolean
                  default: true
                  description: Whether to send the published-items email
                basePath:
                  type: string
                  description: >-
                    Reviewer-tree folder path to publish beneath. Empty string or omitted publishes at the reviewer
                    root.
              additionalProperties: false
      responses:
        '200':
          description: >-
            Publish result — the published items, the total count (including recursive folder contents) and per-item
            errors.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublishItemsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: The caller lacks `canPublishItem` on one or more of the listed resources (ids in `errorData`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
      x-request-source: joi
  /projects/{projectId}/unpublish:
    post:
      summary: Unpublish assets
      description: >
        Bulk unpublish an array of assets, removing their reviewer-tree references. Only assets can be unpublished (a
        folder is removed from the reviewer surface by deleting it there). The caller must hold `canUnpublishItem` on
        every listed resource; otherwise 403 with the offending ids in `errorData`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                resourceIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: Array of asset IDs to unpublish
              additionalProperties: false
      responses:
        '200':
          description: Unpublish result — the unpublished assets, the total count, the removed reviewer paths and per-item errors.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnpublishItemsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: The caller lacks `canUnpublishItem` on one or more of the listed resources (ids in `errorData`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
      x-request-source: joi
  /projects/{projectId}/public:
    post:
      summary: Create a public file system
      description: >
        Creates a public file system (a "release") with a secure token for sharing project assets publicly without
        authentication. `itemPaths` are relative to `project/{projectId}/creator`. Requires `canCreatePublicFileSystem`.


        Gated by the `publicSharing` workspace capability — granted by every paid base plan and intentionally withheld
        from the Demo plan. Requests from workspaces without the capability return 403 `capabilityNotAvailable` with
        `errorData.capability = "publicSharing"`; clients can use this to prompt the user to upgrade their plan.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 0
                  default: []
                  description: Creator-tree item paths to include in the public file system
                  example:
                    - Renders/final.png
                title:
                  type: string
                  maxLength: 100
                  description: Title for the public file system
                  example: Client review – round 1
                description:
                  type: string
                  maxLength: 500
                  description: Optional description for the public file system (may be empty)
                validity:
                  type: number
                  minimum: 0
                  x-exclusiveMinimum: true
                  maximum: 2592000000
                  nullable: true
                  description: >
                    Validity period in milliseconds from now (max 30 days). Omit or pass `null` for a release that never
                    expires.
                hideCreators:
                  type: boolean
                  description: >
                    When `true`, the external public API represses authorship for this release: per-item asset/folder
                    creators AND the release's own "Shared by" creator are omitted from the `/public/{token}*`
                    responses. The stored `creatorId` is unaffected. Defaults to false.
                allowAnonymousComments:
                  type: boolean
                  description: >
                    When `true`, unauthenticated visitors may comment on this release's chats by supplying a display
                    name. Off by default.
                releaseImmediately:
                  type: boolean
                  default: true
                  description: >
                    When `false`, the release is staged as `unreleased` — it exists and its files are copied, but it
                    stays externally inaccessible (the token's public routes return not-found) until it is released via
                    the `/release` endpoint. Defaults to true (goes live immediately).
              required:
                - title
              additionalProperties: false
      responses:
        '200':
          description: Public file system created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicFileSystem'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: |
            Forbidden. May be `capabilityNotAvailable` (workspace is on a plan
            that does not grant `publicSharing` — e.g. Demo) or a permission
            failure from `canCreatePublicFileSystem`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
      x-request-source: joi
    get:
      summary: Get public file systems for a project
      description: >
        Retrieves a paginated list of public file systems for a project with support for search, filtering, and both
        cursor and index-based pagination. Requires `canGetPublicFileSystem`. Cursor-only parameters are rejected with
        index pagination and `page` is rejected with cursor pagination.
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination mode. Cursor-only parameters are rejected when `index`; `page` is rejected when `cursor`.
        - name: search
          in: query
          required: false
          schema:
            type: string
            maxLength: 200
          description: Search public file systems by title and description (case-insensitive substring match; whitespace trimmed).
        - name: status
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                enum:
                  - active
                  - expired
                  - disabled
                  - unreleased
              - type: array
                items:
                  type: string
                  enum:
                    - active
                    - expired
                    - disabled
                    - unreleased
            default:
              - active
              - expired
              - unreleased
          description: >-
            Filter by status (single value, comma-separated string or array). The `unreleased` status is manager-only —
            it is silently stripped for callers without `canCreatePublicFileSystem`.
        - name: creatorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by creator ID
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              updatedAt: -1
          style: deepObject
          explode: true
          description: Sorting criteria for public file systems
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Number of results per page
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (only accepted with `paginate=index`).
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
      responses:
        '200':
          description: Successfully retrieved public file systems. Shape depends on `paginate`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PublicFileSystems'
                  - $ref: '#/components/schemas/CursorPaginatedPublicFileSystems'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      x-request-source: joi
  /projects/{projectId}/public/{publicId}:
    get:
      summary: Get public file system details
      description: >
        Retrieves detailed information about a specific public file system by its ID. Requires `canGetPublicFileSystem`.
        The record must belong to `projectId`; a foreign or unknown id returns `publicFileSystemNotFound` (404).
      tags:
        - Projects
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Public file system retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicFileSystem'
        '404':
          $ref: '#/components/responses/PublicFileSystemNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: publicId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the public file system. Must belong to `projectId`.
      x-request-source: joi
    put:
      summary: Update public file system details
      description: >
        Updates the title, description, validity and sharing flags of an existing public file system. Requires
        `canUpdatePublicFileSystem`; the record must belong to `projectId`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  maxLength: 100
                  description: Updated title for the public file system
                description:
                  type: string
                  maxLength: 500
                  description: Updated description for the public file system (may be empty)
                validity:
                  type: number
                  minimum: 0
                  x-exclusiveMinimum: true
                  maximum: 2592000000
                  nullable: true
                  description: >-
                    Updated validity period in milliseconds from now (max 30 days). Pass `null` to remove the
                    expiration.
                hideCreators:
                  type: boolean
                  description: >
                    Toggle authorship repression for this release. When `true`, the external public API omits per-item
                    asset/folder creators and the release's "Shared by" creator.
                allowAnonymousComments:
                  type: boolean
                  description: Toggle anonymous (display-name only) commenting on this release's chats.
              additionalProperties: false
      responses:
        '200':
          description: Public file system updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicFileSystem'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/PublicFileSystemNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: publicId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the public file system. Must belong to `projectId`.
      x-request-source: joi
    delete:
      summary: Delete a public file system
      description: >
        Permanently deletes a public file system, its copied file-system entries and its inheritance node, and
        invalidates its access token. Requires `canDeletePublicFileSystem`; the record must belong to `projectId`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Public file system deleted successfully. Returns the deleted record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicFileSystem'
        '404':
          $ref: '#/components/responses/PublicFileSystemNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: publicId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the public file system. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/public/{publicId}/add:
    post:
      summary: Add items to public file system
      description: >
        Copies additional creator-tree items (assets or folders) into an existing public file system, marks the assets
        public and recomputes the release inventory. `itemPaths` are relative to `project/{projectId}/creator`. Requires
        `canAddPubicFileSystemItems`; the record must belong to `projectId`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Creator-tree item paths to add to the public file system
                  example:
                    - Renders/extra.png
              required:
                - itemPaths
              additionalProperties: false
      responses:
        '200':
          description: Items added to public file system successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddItemsToPublicFileSystemResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Public file system not found, or an item path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: publicId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the public file system. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/public/{publicId}/release:
    post:
      summary: Release a staged (unreleased) public file system
      description: >-
        Flips a public file system's status from `unreleased` to `active`, making it externally accessible via its
        public token and recomputing the `hasActivePublicFileSystem` flag on its assets. Requires
        `canUpdatePublicFileSystem`. Returns `publicNotUnreleased` (400) if the record isn't in the `unreleased` state.
      tags:
        - Projects
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Public file system released successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicFileSystem'
        '400':
          description: Bad request (e.g. `publicNotUnreleased`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/PublicFileSystemNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: publicId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the public file system. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/public/{token}/files:
    get:
      summary: Get items at the root of a public file system (authenticated management)
      description: >
        Lists the items directly under `public/{publicId}` for the release identified by `token`. This is the
        authenticated management view: unlike the unauthenticated `/public/{token}/files` endpoint it does NOT check
        token expiration or status, and it never populates chats. Requires `canGetPublicFileSystem`. The token must
        belong to `projectId`; otherwise `resourceNotFound` (400). Index pagination is the default; pass
        `paginate=cursor` for cursor pagination (cursor-only parameters are rejected with index pagination and `page` is
        rejected with cursor pagination).
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: '0'
          in: path
          required: true
          schema:
            type: string
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
        - name: path
          in: path
          required: true
          schema:
            type: string
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination mode. Cursor-only parameters are rejected when `index`; `page` is rejected when `cursor`.
        - name: recursiveSearch
          in: query
          required: false
          schema:
            type: boolean
          description: If true, includes items in every sub-folder beneath the path instead of only direct children.
        - name: resourceIds
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by specific resource IDs. May not be combined with `resourceSlugs`.
        - name: resourceSlugs
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  pattern: ^[a-z0-9-]+$
              - type: string
                pattern: ^[a-z0-9-]+$
            default: []
          description: Filter by specific resource slugs. May not be combined with `resourceIds`.
        - name: resourceType
          in: query
          required: false
          schema:
            type: string
            enum:
              - asset
              - folder
          description: Filter by resource type
        - name: resourceTags
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by tag IDs
        - name: creatorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by the resource creator's user ID
        - name: mediaTypes
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  enum:
                    - image
                    - video
                    - audio
                    - folder
                    - file
              - type: string
                enum:
                  - image
                  - video
                  - audio
                  - folder
                  - file
            default: []
          description: Filter by media types (single value or array)
        - name: resourceStatus
          in: query
          required: false
          schema:
            type: string
            default: active
            enum:
              - active
              - pendingDelete
          description: Filter by resource status
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
          description: Partial, case-insensitive match on the resource name
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              mediaType:
                type: number
                enum:
                  - 1
                  - -1
              status:
                type: number
                enum:
                  - 1
                  - -1
              sizeInBytes:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: >-
            Sorting criteria. Keys are mapped onto the FileSystem entry's resource fields (`name` → `resourceName`,
            `createdAt` → `resourceCreatedAt`, ...).
        - name: limit
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            maximum: 100
            default: 10
          description: Number of results per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
        - name: page
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            x-conditionally-required: true
          description: Page number (only accepted with `paginate=index`)
      responses:
        '200':
          description: Successfully retrieved public file system items. Shape depends on `paginate`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/FileSystemItems'
                  - $ref: '#/components/schemas/CursorPaginatedFileSystemItems'
        '400':
          description: Validation error, or no release with this token exists in the project (`resourceNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
  /projects/{projectId}/public/{token}/files/{path}:
    get:
      summary: Get items at a path within a public file system (authenticated management)
      description: >
        Same as the root listing, scoped to a folder path inside the release. Path segments may be folder slugs or IDs
        (IDs are resolved to slugs; an unknown segment returns `fileSystemPathNotFound`, 404). Requires
        `canGetPublicFileSystem`; does NOT check token expiration.
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: '0'
          in: path
          required: true
          schema:
            type: string
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
        - name: path
          in: path
          required: true
          schema:
            type: string
          description: >-
            Folder path within the tree. May contain several `/`-separated segments; each segment is a folder slug or
            ID.
          example: Renders/2024
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination mode. Cursor-only parameters are rejected when `index`; `page` is rejected when `cursor`.
        - name: recursiveSearch
          in: query
          required: false
          schema:
            type: boolean
          description: If true, includes items in every sub-folder beneath the path instead of only direct children.
        - name: resourceIds
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by specific resource IDs. May not be combined with `resourceSlugs`.
        - name: resourceSlugs
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  pattern: ^[a-z0-9-]+$
              - type: string
                pattern: ^[a-z0-9-]+$
            default: []
          description: Filter by specific resource slugs. May not be combined with `resourceIds`.
        - name: resourceType
          in: query
          required: false
          schema:
            type: string
            enum:
              - asset
              - folder
          description: Filter by resource type
        - name: resourceTags
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by tag IDs
        - name: creatorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by the resource creator's user ID
        - name: mediaTypes
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  enum:
                    - image
                    - video
                    - audio
                    - folder
                    - file
              - type: string
                enum:
                  - image
                  - video
                  - audio
                  - folder
                  - file
            default: []
          description: Filter by media types (single value or array)
        - name: resourceStatus
          in: query
          required: false
          schema:
            type: string
            default: active
            enum:
              - active
              - pendingDelete
          description: Filter by resource status
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
          description: Partial, case-insensitive match on the resource name
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              mediaType:
                type: number
                enum:
                  - 1
                  - -1
              status:
                type: number
                enum:
                  - 1
                  - -1
              sizeInBytes:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: >-
            Sorting criteria. Keys are mapped onto the FileSystem entry's resource fields (`name` → `resourceName`,
            `createdAt` → `resourceCreatedAt`, ...).
        - name: limit
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            maximum: 100
            default: 10
          description: Number of results per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
        - name: page
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            x-conditionally-required: true
          description: Page number (only accepted with `paginate=index`)
      responses:
        '200':
          description: Successfully retrieved public file system items at path. Shape depends on `paginate`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/FileSystemItems'
                  - $ref: '#/components/schemas/CursorPaginatedFileSystemItems'
        '400':
          description: Validation error, or no release with this token exists in the project (`resourceNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: A path segment does not exist (`fileSystemPathNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
  /projects/{projectId}/public/{token}/files/move:
    put:
      summary: Move items within public file system
      description: >
        Moves items to a new location within the public file system. Paths are relative to `public/{publicId}`. Items
        already in the destination folder are skipped (`count` may be 0). Requires `canMovePublicFileSystemItems`; the
        token must belong to `projectId`. Does NOT check token expiration.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Array of item paths to move
                  example:
                    - Renders/final.png
                    - Renders/alt
                destinationPath:
                  type: string
                  minLength: 1
                  description: Destination folder path for the items
                  example: Approved
              required:
                - itemPaths
                - destinationPath
              additionalProperties: false
      responses:
        '200':
          description: Items moved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileOperationResponse'
        '400':
          description: Validation error, or no release with this token exists in the project (`resourceNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: A source or destination path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/public/{token}/files/copy:
    put:
      summary: Copy items within public file system
      description: >
        Copies items to a new location within the public file system and recomputes the release inventory. Paths are
        relative to `public/{publicId}`. Requires `canCopyPublicFileSystemItems`; the token must belong to `projectId`.
        Does NOT check token expiration.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Array of item paths to copy
                  example:
                    - Renders/final.png
                destinationPath:
                  type: string
                  minLength: 1
                  description: Destination folder path for the items
                  example: Approved
              required:
                - itemPaths
                - destinationPath
              additionalProperties: false
      responses:
        '200':
          description: Items copied successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileOperationResponse'
        '400':
          description: Validation error, or no release with this token exists in the project (`resourceNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: A source or destination path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/public/{token}/files/delete:
    put:
      summary: Delete items from public file system
      description: >
        Deletes items from the public file system. Paths are relative to `public/{publicId}`. Requires
        `canDeletePublicFileSystemItems`; the token must belong to `projectId`. Does NOT check token expiration.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                itemPaths:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Array of item paths to delete
                  example:
                    - Renders/old.png
              required:
                - itemPaths
              additionalProperties: false
      responses:
        '200':
          description: Items deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileOperationResponse'
        '400':
          description: Validation error, or no release with this token exists in the project (`resourceNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: An item path does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/public/{token}/files/create-folder:
    post:
      summary: Create a folder inside a public file system
      description: >
        Creates a folder at `public/{publicId}/{basePath}` (or the release root when `basePath` is omitted) and
        recomputes the release inventory. Requires `canCreatePublicFileSystemFolder`; the token must belong to
        `projectId`. A `basePath` that does not exist in the release returns `basePathNotFound` (404); a folder with the
        same name already at that address returns `folderExistsAtPath` (409). Does NOT check token expiration.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedNameColorBasePath'
      responses:
        '200':
          description: Folder created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Folder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Public file system not found (`publicFileSystemNotFound`) or `basePathNotFound`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: A folder with this name already exists at the address (`folderExistsAtPath`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/public/{token}/chat:
    get:
      summary: Get main public chat (authenticated management)
      description: |
        Retrieves the main topic chat for a public file system.
        Returns null if no main chat has been created yet (lazy creation pattern).
        Unlike the unauthenticated `/public/{token}/chat` endpoint, this does NOT check
        token expiration. Requires `canGetPublicFileSystem` permission.
      tags:
        - Projects
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Chat retrieved successfully (or null if no chat exists)
          content:
            application/json:
              schema:
                nullable: true
                allOf:
                  - $ref: '#/components/schemas/Chat'
        '400':
          description: No release with this token exists in the project (`resourceNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/public/{token}/assets/{assetId}:
    get:
      summary: Get public asset with chat (authenticated management)
      description: |
        Retrieves an asset along with its public chat in the context of this public file system.
        The chat is returned in `chats.public` and will be null if no chat has been created yet.
        Unlike the unauthenticated `/public/{token}/assets/{assetId}` endpoint, this does NOT
        check token expiration. Requires `canGetPublicFileSystem` permission.
      tags:
        - Projects
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Asset retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Asset'
                  - type: object
                    properties:
                      chats:
                        type: object
                        properties:
                          public:
                            nullable: true
                            allOf:
                              - $ref: '#/components/schemas/Chat'
                            description: The public chat for this asset (null if not created yet)
        '400':
          description: >-
            No release with this token exists in the project (`resourceNotFound`), or the asset record is missing
            (`assetNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The asset is not part of this public file system (`assetNotInPublicFileSystem`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the asset to retrieve.
      x-request-source: joi
  /projects/{projectId}/public/{token}/chat/{chatId}/messages:
    get:
      summary: Get public chat messages (authenticated management)
      description: |
        Retrieves paginated messages for a public chat (the release's main topic chat or
        one of its asset chats). Unlike the unauthenticated
        `/public/{token}/chat/{chatId}/messages` endpoint, this does NOT check token
        expiration. Requires `canGetPublicFileSystem` permission. A chat that does not
        belong to this release returns 403.

        Results are always cursor-paginated; `paginate=index` and `page` are accepted
        but have no effect.
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the chat to get messages from.
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sorting criteria for messages
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Number of messages per page
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: cursor
            enum:
              - cursor
              - index
          description: Pagination mode. Cursor-only parameters are rejected when `index`; `page` is rejected when `cursor`.
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (only accepted with `paginate=index`).
        - name: replyLimit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Number of recent replies to include per message
      responses:
        '200':
          description: Messages retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CursorPaginatedChatMessagesWithChat'
        '400':
          description: Validation error, or no release with this token exists in the project (`resourceNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: The chat does not belong to this public file system.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/ChatNotFound'
      x-request-source: joi
    post:
      summary: Create public chat message (authenticated management)
      description: |
        Creates a message in an existing public chat.
        Unlike the unauthenticated `/public/{token}/chat/{chatId}/messages` endpoint, this does
        NOT check token expiration. Use this for internal management of public file systems.
        Requires `canGetPublicFileSystem` permission. A chat that does not belong to this
        release returns 403. Attachments, mentions, asset mentions and quotes are not
        supported in public chats and are rejected.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
                  minLength: 1
                  maxLength: 10000
                  description: The message content (HTML is sanitised).
                  example: Looks great — approved.
                replyToId:
                  type: string
                  format: uuid
                  description: ID of the message being replied to
                annotations:
                  $ref: '#/components/schemas/AnnotationInputList'
              required:
                - content
              additionalProperties: false
      responses:
        '200':
          description: Message created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMessage'
        '400':
          description: Validation error, or no release with this token exists in the project (`resourceNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: The chat does not belong to this public file system.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/ChatNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the chat to get messages from.
      x-request-source: joi
  /projects/{projectId}/public/{token}/chat/messages:
    post:
      summary: Create public topic chat message with lazy creation (authenticated management)
      description: |
        Creates a message on the main public topic chat. If no main chat exists for this
        public file system, one is created automatically (lazy creation).
        Unlike the unauthenticated `/public/{token}/chat/messages` endpoint, this does
        NOT check token expiration. Use this for internal management of public file systems.
        Requires `canGetPublicFileSystem` permission. Attachments, mentions, asset mentions
        and quotes are not supported in public chats and are rejected.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
                  minLength: 1
                  maxLength: 10000
                  description: The message content (HTML is sanitised).
                  example: Looks great — approved.
                replyToId:
                  type: string
                  format: uuid
                  description: ID of the message being replied to
                annotations:
                  $ref: '#/components/schemas/AnnotationInputList'
              required:
                - content
              additionalProperties: false
      responses:
        '200':
          description: Message created successfully (chat created if needed)
          content:
            application/json:
              schema:
                type: object
                properties:
                  chat:
                    $ref: '#/components/schemas/Chat'
                  message:
                    $ref: '#/components/schemas/ChatMessage'
        '400':
          description: Validation error, or no release with this token exists in the project (`resourceNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
      x-request-source: joi
  /projects/{projectId}/public/{token}/assets/{assetId}/messages:
    post:
      summary: Create public asset chat message with lazy creation (authenticated management)
      description: |
        Creates a message on an asset's public chat. If no public chat exists for the asset
        in this public file system, one is created automatically (lazy creation).
        Unlike the unauthenticated `/public/{token}/assets/{assetId}/messages` endpoint, this does
        NOT check token expiration. Use this for internal management of public file systems.
        Requires `canGetPublicFileSystem` permission. Attachments, mentions, asset mentions
        and quotes are not supported in public chats and are rejected.
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
                  minLength: 1
                  maxLength: 10000
                  description: The message content (HTML is sanitised).
                  example: Looks great — approved.
                replyToId:
                  type: string
                  format: uuid
                  description: ID of the message being replied to
                annotations:
                  $ref: '#/components/schemas/AnnotationInputList'
              required:
                - content
              additionalProperties: false
      responses:
        '200':
          description: Message created successfully (chat created if needed)
          content:
            application/json:
              schema:
                type: object
                properties:
                  chat:
                    $ref: '#/components/schemas/Chat'
                  message:
                    $ref: '#/components/schemas/ChatMessage'
        '400':
          description: Validation error, or no release with this token exists in the project (`resourceNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The asset is not part of this public file system (`assetNotInPublicFileSystem`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
          description: The public access token of the file system. Must belong to `projectId`.
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the asset.
      x-request-source: joi
  /projects/{projectId}/creator/highlighted-messages:
    get:
      summary: Get highlighted messages across the project's creator-visible chats
      description: >
        Cursor-paginated list of active chat messages flagged `highlighted` whose scope is this project and whose scope
        visibility includes `creator`. Each message is populated (author, attachments, etc.) and carries its parent
        `chat`. Requires `canGetCreatorHighlights`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              createdAt: -1
          style: deepObject
          explode: true
          description: Sorting criteria for highlighted messages
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Number of messages per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
      responses:
        '200':
          description: Highlighted messages.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CursorPaginatedChatMessagesWithChat'
        '400':
          $ref: '#/components/responses/BadRequest'
      x-request-source: joi
  /projects/{projectId}/reviewer/highlighted-messages:
    get:
      summary: Get highlighted messages across the project's reviewer-visible chats
      description: >
        Cursor-paginated list of active chat messages flagged `highlighted` whose scope is this project and whose scope
        visibility includes `reviewer`. Each message is populated (author, attachments, etc.) and carries its parent
        `chat`. Requires `canGetReviewerHighlights`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              createdAt: -1
          style: deepObject
          explode: true
          description: Sorting criteria for highlighted messages
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Number of messages per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
      responses:
        '200':
          description: Highlighted messages.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CursorPaginatedChatMessagesWithChat'
        '400':
          $ref: '#/components/responses/BadRequest'
      x-request-source: joi
  /projects/{projectId}/settings/{name}:
    patch:
      summary: Update a single project setting
      description: >
        Updates one key of the project's `settings` JSON column. Mirrors the

        workspace settings endpoint. Requires `canManageProjectSettings`

        (no subscription check on this route).


        At the project level every setting accepts `null`, meaning "inherit the

        workspace's value". Otherwise the type of `value` depends on `name`:


        - `allowAiFeatures`, `aiPolishEnabled`, `aiComposeEnabled`, `aiChatEnabled`,
          `aiAssistEnabled`, `aiTaskGenerationEnabled`, `aiImageRevisionEnabled`,
          `aiPolishAllowReviewer`, `aiComposeAllowReviewer`, `aiChatAllowReviewer`,
          `aiAssistAllowReviewer`, `aiTaskGenerationAllowReviewer`,
          `aiImageRevisionAllowReviewer` — boolean.
        - `aiAssistFollowUpWindow` — integer, 0–30.

        - `aiCustomPreprompt`, `aiTaskGenerationPreprompt` — string (may be empty; empty means "no preprompt", null
        means inherit).

        - `aiChatTools`, `aiAssistTools` — object mapping tool names (`^[a-z][a-z0-9_]*$`) to boolean or null.


        A `value` of the wrong type for the setting is a validation error (400).
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                value:
                  description: >
                    The new value. `null` inherits the workspace's value. Otherwise the type must match the setting
                    named in the path: boolean for the enable / allow-reviewer flags, integer 0–30 for
                    `aiAssistFollowUpWindow`, string (max preprompt length, may be empty) for `aiCustomPreprompt` /
                    `aiTaskGenerationPreprompt`, and an object of tool-name → boolean|null for `aiChatTools` /
                    `aiAssistTools`.
              required:
                - value
              additionalProperties: false
      responses:
        '200':
          description: The updated project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '400':
          description: Validation error (unknown setting name or wrong value type).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the project.
        - name: name
          in: path
          required: true
          schema:
            type: string
            enum:
              - allowAiFeatures
              - aiPolishEnabled
              - aiComposeEnabled
              - aiChatEnabled
              - aiAssistEnabled
              - aiAssistFollowUpWindow
              - aiTaskGenerationEnabled
              - aiImageRevisionEnabled
              - aiPolishAllowReviewer
              - aiComposeAllowReviewer
              - aiChatAllowReviewer
              - aiAssistAllowReviewer
              - aiTaskGenerationAllowReviewer
              - aiImageRevisionAllowReviewer
              - aiCustomPreprompt
              - aiTaskGenerationPreprompt
              - aiChatTools
              - aiAssistTools
          description: The project setting to update. Only the listed names are accepted.
      x-request-source: joi
  /public/{token}:
    get:
      summary: Get public file system details
      description: >
        Retrieves detailed information about a public file system using its access token. No authentication required.
        The token must resolve to an `active`, unexpired release. When the release was created with `hideCreators:
        true`, the `creator` and `creatorId` fields ("Shared by") are omitted from the response.
      tags:
        - Public
      responses:
        '200':
          description: Public file system retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicFileSystem'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: Token is expired (`publicTokenExpired`)
        '404':
          description: Public file system not found, token invalid, or release not active (`notFound`)
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
            example: abc123xyz0
          description: The 10-character access token for the public file system.
      x-request-source: joi
  /public/{token}/files:
    get:
      summary: Get public file system items
      description: >
        Retrieves items (assets and folders) from a public file system at the root level. No authentication required.
        Supports index pagination (default) or cursor pagination via `paginate=cursor`; the cursor-only options are
        rejected with 400 when `paginate` is `index`, and `page` is rejected when `paginate` is `cursor`. Populated
        resources are reduced to the public allowlist before being returned. When the release was created with
        `hideCreators: true`, each item's `creator` / `resourceCreator` / `resourceCreatorId` fields are omitted.
      tags:
        - Public
      parameters:
        - name: '0'
          in: path
          required: true
          schema:
            type: string
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
            example: abc123xyz0
          description: The 10-character access token for the public file system.
        - name: path
          in: path
          required: true
          schema:
            type: string
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination type to use
        - name: recursiveSearch
          in: query
          required: false
          schema:
            type: boolean
          description: Whether to search recursively through subfolders
        - name: resourceIds
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Restrict results to these asset/folder IDs. A single value or repeated parameter.
        - name: resourceSlugs
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  pattern: ^[a-z0-9-]+$
              - type: string
                pattern: ^[a-z0-9-]+$
            default: []
          description: Restrict results to these resource slugs. A single value or repeated parameter.
        - name: resourceType
          in: query
          required: false
          schema:
            type: string
            enum:
              - asset
              - folder
          description: Filter by resource type
        - name: resourceTags
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by tag IDs. A single value or repeated parameter.
        - name: creatorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by the ID of the user who created the resource
        - name: mediaTypes
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  enum:
                    - image
                    - video
                    - audio
                    - folder
                    - file
              - type: string
                enum:
                  - image
                  - video
                  - audio
                  - folder
                  - file
            default: []
          description: Filter by media types. A single value or repeated parameter.
        - name: resourceStatus
          in: query
          required: false
          schema:
            type: string
            default: active
            enum:
              - active
              - pendingDelete
          description: Filter by resource status
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
          description: Search items by name
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              mediaType:
                type: number
                enum:
                  - 1
                  - -1
              status:
                type: number
                enum:
                  - 1
                  - -1
              sizeInBytes:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sort order, e.g. `sort[name]=1`. Keys are mapped onto the underlying file-system item fields.
        - name: limit
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            maximum: 100
            default: 10
          description: Number of items per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (cursor pagination only; rejected when `paginate=index`)
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to paginate in reverse order (cursor pagination only)
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to include total counts (cursor pagination only)
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to include the cursor record itself (cursor pagination only)
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: Start at a specific file-system item ID (cursor pagination only)
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to include the startAt record (cursor pagination only)
        - name: page
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            x-conditionally-required: true
          description: Page number (index pagination only; rejected when `paginate=cursor`)
      responses:
        '200':
          description: Items retrieved successfully
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/FileSystemItems'
                  - $ref: '#/components/schemas/PublicCursorPaginatedFileSystemItems'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: Token is expired (`publicTokenExpired`)
        '404':
          description: Public file system not found, token invalid, or release not active
      x-request-source: joi
  /public/{token}/files/{path}:
    get:
      summary: Get public file system items at path
      description: >
        Retrieves items (assets and folders) from a public file system at a specific folder path. No authentication
        required. Accepts the same query parameters as `GET /public/{token}/files`. When the release was created with
        `hideCreators: true`, each item's creator fields are omitted.
      tags:
        - Public
      parameters:
        - name: '0'
          in: path
          required: true
          schema:
            type: string
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
            example: abc123xyz0
          description: The 10-character access token for the public file system.
        - name: path
          in: path
          required: true
          schema:
            type: string
          description: >
            The folder path to browse (supports nested paths). Segments may be folder slugs or folder IDs; IDs are
            resolved to slugs before lookup.
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination type to use
        - name: recursiveSearch
          in: query
          required: false
          schema:
            type: boolean
          description: Whether to search recursively through subfolders
        - name: resourceIds
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Restrict results to these asset/folder IDs. A single value or repeated parameter.
        - name: resourceSlugs
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  pattern: ^[a-z0-9-]+$
              - type: string
                pattern: ^[a-z0-9-]+$
            default: []
          description: Restrict results to these resource slugs. A single value or repeated parameter.
        - name: resourceType
          in: query
          required: false
          schema:
            type: string
            enum:
              - asset
              - folder
          description: Filter by resource type
        - name: resourceTags
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  format: uuid
              - type: string
                format: uuid
            default: []
          description: Filter by tag IDs. A single value or repeated parameter.
        - name: creatorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by the ID of the user who created the resource
        - name: mediaTypes
          in: query
          required: false
          schema:
            oneOf:
              - type: array
                items:
                  type: string
                  enum:
                    - image
                    - video
                    - audio
                    - folder
                    - file
              - type: string
                enum:
                  - image
                  - video
                  - audio
                  - folder
                  - file
            default: []
          description: Filter by media types. A single value or repeated parameter.
        - name: resourceStatus
          in: query
          required: false
          schema:
            type: string
            default: active
            enum:
              - active
              - pendingDelete
          description: Filter by resource status
        - name: nameSearch
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
          description: Search items by name
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              mediaType:
                type: number
                enum:
                  - 1
                  - -1
              status:
                type: number
                enum:
                  - 1
                  - -1
              sizeInBytes:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sort order, e.g. `sort[name]=1`. Keys are mapped onto the underlying file-system item fields.
        - name: limit
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            maximum: 100
            default: 10
          description: Number of items per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (cursor pagination only; rejected when `paginate=index`)
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to paginate in reverse order (cursor pagination only)
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to include total counts (cursor pagination only)
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to include the cursor record itself (cursor pagination only)
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: Start at a specific file-system item ID (cursor pagination only)
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to include the startAt record (cursor pagination only)
        - name: page
          in: query
          required: false
          schema:
            type: number
            minimum: 1
            x-conditionally-required: true
          description: Page number (index pagination only; rejected when `paginate=cursor`)
      responses:
        '200':
          description: Items retrieved successfully
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/FileSystemItems'
                  - $ref: '#/components/schemas/PublicCursorPaginatedFileSystemItems'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: Token is expired (`publicTokenExpired`)
        '404':
          description: Public file system not found, token invalid, release not active, or path not found
      x-request-source: joi
  /public/{token}/download:
    post:
      summary: Generate download links for assets
      description: >
        Generates signed download URLs for the original files of assets in a public file system. No authentication
        required. Every requested asset must exist within the release's file-system path, otherwise the whole request is
        rejected with 400 `assetNotFound`. One result object is returned per requested asset; an individual entry may
        carry `status: fail` (with an `error` code) when its original file could not be signed.
      tags:
        - Public
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                assetIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                  minItems: 1
                  maxItems: 100
                  description: Array of asset IDs to generate download links for
              required:
                - assetIds
              additionalProperties: false
      responses:
        '200':
          description: Download links generated successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  oneOf:
                    - $ref: '#/components/schemas/DownloadSignedUrlData'
                    - type: object
                      description: Per-asset failure entry
                      properties:
                        ownerAssetId:
                          $ref: '#/components/schemas/UUID'
                        status:
                          type: string
                          enum:
                            - fail
                        error:
                          type: string
                          example: originalNotFound
        '400':
          description: Validation error, or one or more assets are not part of this public file system (`assetNotFound`)
        '403':
          description: Token is expired (`publicTokenExpired`)
        '404':
          description: Public file system not found, token invalid, or release not active
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
            example: abc123xyz0
          description: The 10-character access token for the public file system.
      x-request-source: joi
  /public/{token}/chat:
    get:
      summary: Get main public file system chat
      description: |
        Retrieves the main topic chat for a public file system.
        Returns null if no main chat has been created yet (lazy creation pattern).
        No authentication required for read access.
      tags:
        - Public
      responses:
        '200':
          description: Chat retrieved successfully (or null if no chat exists)
          content:
            application/json:
              schema:
                nullable: true
                allOf:
                  - $ref: '#/components/schemas/Chat'
        '403':
          description: Token is expired (`publicTokenExpired`) or the release is disabled (`publicDisabled`)
        '404':
          description: Public file system not found or token invalid (`publicNotFound`)
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
            example: abc123xyz0
          description: The 10-character access token for the public file system.
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      x-request-source: joi
  /public/{token}/chat/messages:
    post:
      summary: Create message on the main public chat (lazy creation)
      description: |
        Creates a message on the public file system's main topic chat. If the
        main chat does not exist yet it is created automatically (lazy creation
        pattern), using the release title as its subject.

        **Authentication is optional.** A signed-in user posts with their real
        identity (`authorId`); an unauthenticated visitor may post anonymously by
        supplying `guestName` when the release has `allowAnonymousComments` enabled.
        See the `guestId` / `guestColor` notes on `POST /public/{token}/chat/{chatId}/messages`.
        Attachments, mentions, asset mentions and quotes are not supported in
        public chats and are rejected with 400.
      tags:
        - Public
      security:
        - {}
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedContentReplyToIdAnnotations'
      responses:
        '200':
          description: Chat and message created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  chat:
                    allOf:
                      - $ref: '#/components/schemas/Chat'
                    description: The main topic chat (created if it didn't exist)
                  message:
                    allOf:
                      - $ref: '#/components/schemas/ChatMessage'
                    description: The created message
        '400':
          description: Validation error, or a display name is required to comment anonymously (`guestNameRequired`)
        '403':
          description: |
            Token is expired (`publicTokenExpired`), the release is disabled
            (`publicDisabled`), or anonymous comments are not enabled for this
            release (`anonymousCommentsDisabled`).
        '404':
          description: Public file system not found or token invalid (`publicNotFound`)
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
            example: abc123xyz0
          description: The 10-character access token for the public file system.
      x-request-source: joi
  /public/{token}/chat/{chatId}/messages:
    get:
      summary: Get public chat messages
      description: |
        Retrieves paginated messages for a public chat. The chat must be the
        release's main topic chat or one of its asset public chats, otherwise
        403 `forbidden` is returned.
        No authentication required for read access.

        Messages are always returned cursor-paginated. `paginate=index` and
        `page` are accepted by validation but are ignored by the handler.
      tags:
        - Public
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
            example: abc123xyz0
          description: The 10-character access token for the public file system.
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the chat to get messages from or post to.
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              id: -1
          style: deepObject
          explode: true
          description: Sort order, e.g. `sort[createdAt]=-1`.
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Number of messages per page
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: cursor
            enum:
              - cursor
              - index
          description: Pagination style. Only `cursor` is honoured by the handler.
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (cursor mode only)
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to paginate in reverse order (cursor mode only)
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to include total counts (cursor mode only)
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to include the cursor record itself (cursor mode only)
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: Start at specific message ID (cursor mode only)
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: Whether to include the startAt record (cursor mode only)
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (index mode only; accepted but ignored)
        - name: replies
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 10
          description: Number of recent replies to include per message
      responses:
        '200':
          description: Messages retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CursorPaginatedMessages'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: >-
            Token is expired (`publicTokenExpired`), release is disabled (`publicDisabled`), or the chat does not belong
            to this release (`forbidden`)
        '404':
          description: Public file system not found (`publicNotFound`) or chat not found (`chatNotFound`)
      x-request-source: joi
    post:
      summary: Create message in public chat
      description: |
        Creates a new message in an existing public chat. The chat must be the
        release's main topic chat or one of its asset public chats.

        **Authentication is optional.** A signed-in user posts with their real
        identity (`authorId`). An unauthenticated visitor may post anonymously by
        supplying a display name (`guestName`) — but only when the release has
        `allowAnonymousComments` enabled. The client should also send a stable
        `guestId` (a UUID it persists in localStorage per token) so two guests who
        type the same name stay distinct, plus an optional `guestColor`. Guest
        fields are ignored when the caller is authenticated.
        Attachments, mentions, asset mentions and quotes are not supported in
        public chats and are rejected with 400.
      tags:
        - Public
      security:
        - {}
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedContentReplyToIdAnnotations'
      responses:
        '200':
          description: Message created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatMessage'
        '400':
          description: Validation error, or a display name is required to comment anonymously (`guestNameRequired`)
        '403':
          description: |
            Token is expired (`publicTokenExpired`), the release is disabled
            (`publicDisabled`), anonymous comments are not enabled for this
            release (`anonymousCommentsDisabled`), or the chat does not belong to
            this release (`forbidden`).
        '404':
          description: Public file system not found (`publicNotFound`) or chat not found (`chatNotFound`)
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
            example: abc123xyz0
          description: The 10-character access token for the public file system.
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the chat to get messages from or post to.
      x-request-source: joi
  /public/{token}/assets/{assetId}:
    get:
      summary: Get asset with public chat
      description: |
        Retrieves an asset along with its public chat in the context of this public file system.
        The chat is returned in `chats.public` and will be null if no chat has been created yet.
        No authentication required for read access.
        When the release was created with `hideCreators: true`, the asset's creator fields are omitted.
      tags:
        - Public
      responses:
        '200':
          description: Asset retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Asset'
                  - type: object
                    properties:
                      chats:
                        type: object
                        properties:
                          public:
                            nullable: true
                            allOf:
                              - $ref: '#/components/schemas/Chat'
                            description: The public chat for this asset (null if not created yet)
        '400':
          description: Asset record not found (`assetNotFound`)
        '403':
          description: Token is expired (`publicTokenExpired`) or the release is disabled (`publicDisabled`)
        '404':
          description: >-
            Public file system not found (`publicNotFound`) or asset is not part of this public file system
            (`assetNotInPublicFileSystem`)
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
            example: abc123xyz0
          description: The 10-character access token for the public file system.
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the asset to retrieve.
      x-request-source: joi
  /public/{token}/assets/{assetId}/messages:
    post:
      summary: Create message on asset's public chat (lazy creation)
      description: |
        Creates a message on an asset's public chat. If no public chat exists for this asset
        in this public file system, one is created automatically (lazy creation pattern).

        **Authentication is optional.** A signed-in user posts with their real
        identity (`authorId`); an unauthenticated visitor may post anonymously by
        supplying `guestName` when the release has `allowAnonymousComments` enabled.
        See the `guestId` / `guestColor` notes on `POST /public/{token}/chat/{chatId}/messages`.
        Attachments, mentions, asset mentions and quotes are not supported in
        public chats and are rejected with 400.
      tags:
        - Public
      security:
        - {}
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedContentReplyToIdAnnotations'
      responses:
        '200':
          description: Chat and message created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  chat:
                    allOf:
                      - $ref: '#/components/schemas/Chat'
                    description: The chat (created if it didn't exist)
                  message:
                    allOf:
                      - $ref: '#/components/schemas/ChatMessage'
                    description: The created message
        '400':
          description: Validation error, or a display name is required to comment anonymously (`guestNameRequired`)
        '403':
          description: |
            Token is expired (`publicTokenExpired`), the release is disabled
            (`publicDisabled`), or anonymous comments are not enabled for this
            release (`anonymousCommentsDisabled`).
        '404':
          description: >-
            Public file system not found (`publicNotFound`) or asset is not part of this public file system
            (`assetNotInPublicFileSystem`)
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]{10}$
            example: abc123xyz0
          description: The 10-character access token for the public file system.
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the asset to post to.
      x-request-source: joi
  /assets/{assetId}/public-links:
    post:
      summary: Create a public download link for an asset
      description: |
        Creates a temporary public download link for an asset. The link can optionally have
        an expiration. Requires the `canCreatePublicLink` permission (project/workspace admin roles)
        AND the `publicSharing` workspace capability — granted by every paid base plan and
        intentionally withheld from the Demo plan. Workspaces without the capability receive
        403 `capabilityNotAvailable` with `errorData.capability = "publicSharing"`, which
        clients can use to prompt for an upgrade.

        Sets `hasActivePublicLink`, `everPublic`, and `publicAssetLinkIds` on the asset.
        Sends a notification and email to workspace admins if the creator is a project-level admin.
      tags:
        - PublicAssetLinks
        - Assets
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset to create a public link for.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                validity:
                  type: integer
                  minimum: 60000
                  description: |
                    Link validity in milliseconds. If omitted, the link does not expire.
                  example: 86400000
                projectId:
                  type: string
                  format: uuid
                  description: The project ID the asset belongs to.
                mode:
                  type: string
                  default: embed-download
                  enum:
                    - download
                    - embed
                    - embed-download
                  description: |
                    Capability mode for the link. Enforced server-side at
                    `/v1/public-download/{token}/download` and `/embed-files`.
                    - `download` — direct-download link only; embed iframe blocked.
                    - `embed` — embeddable iframe only; direct download blocked.
                    - `embed-download` — both endpoints allowed (default; backward-compatible).
              required:
                - projectId
              additionalProperties: false
      responses:
        '201':
          description: Public link created successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicAssetLink'
        '400':
          description: Bad request - Invalid asset ID or request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized - Authentication required.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: |
            Forbidden. May be `capabilityNotAvailable` (workspace is on a plan
            that does not grant `publicSharing` — e.g. Demo) or a permission
            failure from `canCreatePublicLink`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
      x-request-source: joi
    get:
      summary: List all public download links for an asset
      description: |
        Returns all public download links for an asset, including active, disabled, and expired links.
        Requires canGetPublicLinks permission.
      tags:
        - PublicAssetLinks
        - Assets
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset.
      responses:
        '200':
          description: List of public links.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/PublicAssetLink'
        '401':
          description: Unauthorized - Authentication required.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden - User does not have canGetPublicLinks permission.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
      x-request-source: joi
  /assets/{assetId}/public-links/{linkId}:
    put:
      summary: Update a public download link
      description: |
        Update a public link's expiration or status. At least one field must be provided.
        Requires canUpdatePublicLink permission.
      tags:
        - PublicAssetLinks
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: linkId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                validity:
                  type: integer
                  minimum: 60000
                  description: New validity in milliseconds from now.
                status:
                  type: string
                  enum:
                    - active
                    - disabled
                  description: New status for the link.
                mode:
                  type: string
                  enum:
                    - download
                    - embed
                    - embed-download
                  description: Update the link's capability mode.
              additionalProperties: false
              minProperties: 1
      responses:
        '200':
          description: Link updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicAssetLink'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Public asset link not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
      x-request-source: joi
  /assets/{assetId}/public-links/{linkId}/disable:
    put:
      summary: Disable a public download link
      description: |
        Sets the link status to 'disabled'. The link can no longer be used for downloads.
        Updates hasActivePublicLink on the asset. Requires canUpdatePublicLink permission.
      tags:
        - PublicAssetLinks
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: linkId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Link disabled successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicAssetLink'
        '404':
          description: Public asset link not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
      x-request-source: joi
  /assets/{assetId}/public-links/{linkId}/reactivate:
    put:
      summary: Reactivate a disabled or expired public download link
      description: |
        Sets the link status back to 'active' with an optional new expiration.
        Updates hasActivePublicLink on the asset. Requires canUpdatePublicLink permission.
      tags:
        - PublicAssetLinks
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: linkId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                validity:
                  type: integer
                  minimum: 60000
                  description: New validity in milliseconds from now. If omitted, no expiration.
              additionalProperties: false
      responses:
        '200':
          description: Link reactivated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicAssetLink'
        '404':
          description: Public asset link not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
      x-request-source: joi
  /public-download/{token}:
    get:
      summary: Resolve a public download token
      description: |
        Resolves a public download token to minimal file information for the download page.
        No authentication required. Returns fileName, mediaType, status, the link mode,
        and (for image assets) a `previewKeyPath` so the page can render an inline preview.
      tags:
        - PublicAssetLinks
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]+$
            example: Ab3dEf9HiJ
          description: The 10-character alphanumeric public download token.
      responses:
        '200':
          description: Token resolved successfully.
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                    description: The public download token.
                  fileName:
                    type: string
                    description: Name of the file.
                    example: my-video.mp4
                  mediaType:
                    type: string
                    description: Media type of the asset.
                    example: video
                  status:
                    type: string
                    enum:
                      - active
                    description: Status of the link.
                  mode:
                    type: string
                    enum:
                      - download
                      - embed
                      - embed-download
                    description: |
                      Capability mode of the link. Clients use this to decide whether to
                      render an inline preview (`embed-download`), an embed-only
                      message (`embed`), or start the download directly (`download`).
                    example: embed-download
                  mimeType:
                    type: string
                    nullable: true
                    description: Mime type of the original media file, when known.
                    example: image/png
                  previewKeyPath:
                    type: string
                    nullable: true
                    description: |
                      For image assets: keyPath of the image file. Resolve it against the
                      base media URL to render an inline preview. `null` for non-image
                      assets (video/audio previews embed the player page; other files
                      render an icon).
        '400':
          description: The underlying asset is no longer active (`assetNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Public asset link not found (`publicAssetLinkNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '410':
          description: Link has expired (`publicAssetLinkExpired`) or is disabled (`publicAssetLinkInactive`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
  /public-download/{token}/download:
    get:
      summary: Get a signed download URL for a public download token
      description: |
        Generates a signed download URL for the asset's original file.
        No authentication required. The URL is temporary and expires.
      tags:
        - PublicAssetLinks
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]+$
          description: The 10-character alphanumeric public download token.
      responses:
        '200':
          description: Signed download URL generated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    description: Signed download URL.
                  fileName:
                    type: string
                    description: Name of the file for download.
                  mimeType:
                    type: string
                    description: MIME type of the file.
                    example: video/mp4
                  expires:
                    type: integer
                    description: Unix timestamp (seconds) when the signed URL expires.
        '403':
          description: The link is `embed`-only and does not allow direct downloads (`publicAssetLinkEmbedOnly`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Public asset link not found (`publicAssetLinkNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '410':
          description: Link has expired (`publicAssetLinkExpired`) or is disabled (`publicAssetLinkInactive`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: The asset or its original file is missing, so no signed URL could be produced (`unknownError`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
  /public-download/{token}/embed-files:
    get:
      summary: Resolve a public asset link to embed-player file paths
      description: |
        Returns the HLS streaming manifest keyPath and a fallback original-media keyPath
        for the asset behind a public download token. This is what the embeddable
        player at `/public/{token}/embed` uses.

        Resolve keyPaths against the base media URL. Play the `.m3u8` manifest with an
        HLS-capable player when `streamKeyPath` is present, falling back to the original
        media file otherwise. Only `video` and `audio` assets are meant to be embedded;
        the response still returns paths for other media types, but they should not be
        embedded.

        No authentication required. Returns minimal file info — never the full asset
        record (no `id`, `files`, `creatorId`, etc.).
      tags:
        - PublicAssetLinks
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            maxLength: 10
            pattern: ^[a-zA-Z0-9]+$
            example: Ab3dEf9HiJ
          description: The 10-character alphanumeric public download token.
      responses:
        '200':
          description: Embed file paths resolved.
          content:
            application/json:
              schema:
                type: object
                required:
                  - fileName
                  - mediaType
                  - streamKeyPath
                  - mediaKeyPath
                  - posterKeyPath
                  - waveformKeyPath
                properties:
                  fileName:
                    type: string
                    description: Asset name; use as the iframe `title` for accessibility.
                    example: my-video.mp4
                  mediaType:
                    type: string
                    enum:
                      - video
                      - audio
                      - image
                      - file
                    description: |
                      Media type of the asset. Embed UI is only exposed for `video` and `audio`;
                      other types are returned for completeness but should not be embedded.
                    example: video
                  mode:
                    type: string
                    enum:
                      - embed
                      - embed-download
                    description: |
                      Capability mode of the underlying public link. `embed-download` allows
                      both embedding and direct download; `embed` blocks direct download.
                      `download`-only links never reach this endpoint (rejected with 403).
                      Clients can use this to show or hide a download control.
                    example: embed-download
                  streamKeyPath:
                    type: string
                    nullable: true
                    description: |
                      HLS manifest keyPath (e.g. `workspaces/{ws}/.../stream/index.m3u8`),
                      `null` when post-processing has not produced an HLS rendition yet.
                      Resolve it against the base media URL and play it with an HLS-capable player.
                    example: /workspaces/abc/assets/123/stream/index.m3u8
                  mediaKeyPath:
                    type: string
                    nullable: true
                    description: |
                      Original media file keyPath, used as the fallback when `streamKeyPath` is null.
                      `null` when the asset has not finished uploading or its files are inactive.
                    example: /workspaces/abc/assets/123/media/my-video.mp4
                  posterKeyPath:
                    type: string
                    nullable: true
                    description: |
                      Poster image keyPath to use as the video poster so the embed
                      shows a still frame before playback starts. Precedence: user-supplied
                      `customThumbnail` (largest by size), then explicit `poster` file, then largest
                      auto-generated `thumbnail` file. `null` when none is available (e.g. audio-only
                      assets, or video with no generated thumbnails yet).

                      When the picked poster is a `customThumbnail`, the keyPath is suffixed with
                      `?v={fileId}` as a cache-buster — custom thumbnails reuse the same storage path
                      across re-uploads, and CDNs/browsers would otherwise serve the previous
                      version indefinitely.
                    example: /workspaces/abc/assets/123/customThumbnail/large.jpg?v=019e10aa-0000-7000-8000-000000000002
                  waveformKeyPath:
                    type: string
                    nullable: true
                    description: |
                      Pre-computed waveform peaks keyPath (audio assets), so an audio player can
                      render the waveform without decoding the stream. `null` for non-audio
                      assets or audio that hasn't been post-processed.
                    example: /workspaces/abc/assets/123/waveform/peaks.json
        '400':
          description: The underlying asset is no longer active (`assetNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: The link is `download`-only and does not allow embedding (`publicAssetLinkDownloadOnly`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Public asset link not found (`publicAssetLinkNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '410':
          description: Link has expired (`publicAssetLinkExpired`) or is disabled (`publicAssetLinkInactive`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
  /scratch/{scratchId}/complete-upload:
    post:
      tags:
        - Scratch
      summary: Finish a multipart upload for a scratch object.
      description: |
        Same body as `POST /v1/assets/complete-upload`. Commits the
        multipart upload, records the final object size against the
        workspace's storage usage, and moves the scratch object to
        `active`. Only the user who created the scratch object may call
        this (403 otherwise). Idempotent: calling it on an
        already-`active` object returns the same `{ id, url }` without
        changing anything.
      security:
        - bearerAuth: []
      parameters:
        - name: scratchId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                uploadId:
                  type: string
                  description: Multipart upload ID returned with the signed upload URL bundle.
                parts:
                  type: array
                  items:
                    type: object
                    properties:
                      ETag:
                        type: string
                      PartNumber:
                        type: integer
                        minimum: 1
                    required:
                      - ETag
                      - PartNumber
                    additionalProperties: false
                  minItems: 1
              required:
                - uploadId
                - parts
              additionalProperties: false
      responses:
        '200':
          description: Upload committed. The scratch object is now `active` and its size counts toward workspace storage.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  url:
                    type: string
                    nullable: true
        '400':
          description: |
            Bad request. `fileInvalid` if `uploadId` or `parts` is
            missing/empty. `notSupported` if the row is neither
            `pendingUpload` nor `active` (e.g. already promoted / expired).
        '403':
          description: Caller is not the creator.
        '404':
          description: Scratch object not found, or the uploaded object could not be verified in storage.
        '500':
          description: '`unknownError` if the multipart upload could not be committed.'
      x-request-source: joi
  /scratch/{scratchId}/promote:
    post:
      tags:
        - Scratch
      summary: Promote a scratch object to a real Asset.
      description: |
        Materialises an `active` scratch object as an Asset
        (`functionType: media`) in the workspace or project the scratch
        object was created in. The destination is derived from the scratch
        object itself — a project asset when it was created within a
        project, otherwise a workspace asset — and is **not** accepted from
        the request body, so a client cannot claim a scratch object belongs
        to a different workspace or project.

        Only the user who created the scratch object may promote it, and
        they must also hold `canCreateAsset` on the destination workspace
        or project.

        Idempotent: promoting an object that was already promoted returns
        the existing asset with `alreadyPromoted: true`, so a client can
        tell "freshly added" from "already added". To attach a scratch
        object to a chat message instead, pass it as a `{ scratchId, name? }`
        attachment when sending the message
        (`POST /v1/chats/{chatId}/messages`).
      security:
        - bearerAuth: []
      parameters:
        - name: scratchId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                fileName:
                  type: string
                  minLength: 1
                  maxLength: 200
                  description: Optional user-facing name for the resulting Asset. Defaults to the scratch object's own name.
                  example: hero-shot-v2.png
              additionalProperties: false
      responses:
        '200':
          description: Scratch promoted (or already promoted).
          content:
            application/json:
              schema:
                type: object
                required:
                  - asset
                  - alreadyPromoted
                properties:
                  asset:
                    $ref: '#/components/schemas/Asset'
                  alreadyPromoted:
                    type: boolean
                    description: '`true` when the scratch object had already been promoted and the existing asset is returned.'
        '403':
          description: Caller is not the creator, or lacks `canCreateAsset` on the destination resource (`forbidden`).
        '404':
          description: Scratch not found (`notFound`).
        '500':
          description: Promotion failed for a non-API reason (`aiRevisionPromotionFailed`).
      x-request-source: joi
  /settings:
    get:
      summary: Get all resource settings
      description: Returns all resource-specific setting overrides for the current user
      tags:
        - Settings
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully retrieved all resource settings
          content:
            application/json:
              schema:
                type: object
                properties:
                  resourceSettings:
                    type: array
                    items:
                      $ref: '#/components/schemas/ResourceSettings'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /settings/{resourceType}/{resourceId}/effective:
    get:
      summary: Get effective settings for a resource
      description: >-
        Returns resolved settings with cascade from global (user preferences) → workspace → project. Only the caller's
        own settings are resolved.
      tags:
        - Settings
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully retrieved effective settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EffectiveSettings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/UserNotFound'
      parameters:
        - name: resourceType
          in: path
          required: true
          schema:
            type: string
            enum:
              - workspace
              - project
          description: The resource type (workspace or project)
        - name: resourceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The resource ID
        - name: workspaceId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: >-
            Parent workspace id. Only meaningful when resourceType is 'project' — when supplied, that workspace's
            overrides are applied between the global and project layers; when omitted the workspace layer is skipped.
      x-request-source: joi
  /settings/{resourceType}/{resourceId}:
    get:
      summary: Get raw resource settings
      description: >-
        Returns only the caller's overrides for this specific resource (not cascaded). When no overrides exist a `{
        message }` object is returned instead (still 200).
      tags:
        - Settings
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully retrieved resource settings
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ResourceSettings'
                  - type: object
                    properties:
                      message:
                        type: string
                        example: No custom settings for this resource
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/UserNotFound'
      parameters:
        - name: resourceType
          in: path
          required: true
          schema:
            type: string
            enum:
              - workspace
              - project
          description: The resource type (workspace or project)
        - name: resourceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The resource ID
      x-request-source: joi
    put:
      summary: Update resource settings
      description: >-
        Create or update the caller's setting overrides for a specific resource. Each supplied category is
        shallow-merged into the existing overrides for that resource; at least one category is required. No check is
        made that the caller is a member of the resource.
      tags:
        - Settings
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                emailNotifications:
                  type: object
                  additionalProperties:
                    oneOf:
                      - type: boolean
                      - type: number
                      - nullable: true
                systemNotifications:
                  type: object
                  additionalProperties:
                    oneOf:
                      - type: boolean
                      - nullable: true
                sounds:
                  type: object
                  additionalProperties:
                    oneOf:
                      - type: boolean
                      - nullable: true
              additionalProperties: false
              minProperties: 1
              description: |
                Body for `PUT /settings/{resourceType}/{resourceId}`. Each category is a
                free-form map — any setting key is accepted (the well-known keys are
                listed in `ResourceSettingsOverrides`). `null` clears an override so the
                parent level applies again. `emailNotifications` values may be boolean,
                number (interval settings) or null; the other categories accept boolean
                or null.
              example:
                emailNotifications:
                  newMention: false
                  newMentionActiveInterval: 30
                sounds:
                  playSounds: null
      responses:
        '200':
          description: Successfully updated resource settings
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  settings:
                    $ref: '#/components/schemas/ResourceSettings'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
      parameters:
        - name: resourceType
          in: path
          required: true
          schema:
            type: string
            enum:
              - workspace
              - project
          description: The resource type (workspace or project)
        - name: resourceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The resource ID
      x-request-source: joi
    delete:
      summary: Reset resource settings
      description: Remove all setting overrides for this resource
      tags:
        - Settings
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully reset resource settings
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
      parameters:
        - name: resourceType
          in: path
          required: true
          schema:
            type: string
            enum:
              - workspace
              - project
          description: The resource type (workspace or project)
        - name: resourceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The resource ID
      x-request-source: joi
  /shortlink/{code}:
    get:
      summary: Resolve a short link
      description: |
        Resolves a short link code to its full deep link. This endpoint is publicly accessible
        and does not require authentication. The deep link can be used to navigate directly to
        a resource in the Nurama web app.
      tags:
        - ShortLinks
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
            minLength: 8
            maxLength: 8
            pattern: ^[a-zA-Z0-9]+$
            example: Ab3dEf9H
          description: The 8-character alphanumeric short link code.
      responses:
        '200':
          description: Successfully resolved the short link.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deepLink:
                    type: string
                    description: The full deep link URL for navigation (an absolute URL on the Nurama web app origin).
                    example: >-
                      https://app.nurama.com/workspaces/my-workspace/projects/my-project/creator/assets/019e10aa-0000-7000-8000-000000000001
                  resourceType:
                    type: string
                    enum:
                      - asset
                      - project
                      - workspace
                      - chatMessage
                      - chatSubmission
                    description: The type of the linked resource. Links created via the API are `asset` or `chatMessage`.
                    example: asset
                  visibility:
                    type: string
                    enum:
                      - creator
                      - reviewer
                      - null
                    nullable: true
                    description: The visibility context for the resource (if applicable).
                    example: creator
        '400':
          description: Bad request - Invalid short link code format.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Short link not found (`shortLinkNotFound`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-request-source: joi
  /assets/{assetId}/shortlink:
    post:
      summary: Create a short link for an asset
      description: |
        Creates a short link for an asset. If a short link already exists for the asset
        with the same visibility setting, the existing short link is returned.
        Requires `canGetAsset` on the asset.
      tags:
        - ShortLinks
        - Assets
      parameters:
        - name: assetId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the asset to create a short link for.
        - name: visibility
          in: query
          required: false
          schema:
            type: string
            enum:
              - creator
              - reviewer
          description: |
            Optional visibility context for the short link. If specified, the deep link
            will include this visibility in the path. If not specified, the deep link
            will use the default visibility for the resource.
      responses:
        '201':
          description: Short link created or existing short link returned.
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                    pattern: ^[a-zA-Z0-9]{8}$
                    description: The 8-character alphanumeric short link code.
                    example: Ab3dEf9H
                  shortUrl:
                    type: string
                    description: The complete short URL ready for sharing.
                    example: https://app.nurama.com/s/Ab3dEf9H
        '400':
          description: Bad request - Invalid asset ID or visibility.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized - Authentication required.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden - User does not have permission to access the asset.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Asset not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
      x-request-source: joi
  /chats/message/{messageId}/shortlink:
    post:
      summary: Create a short link for a chat message
      description: |
        Creates a short link for a chat message. If a short link already exists for the message,
        the existing short link is returned. Visibility is automatically inherited from the
        parent chat (e.g., 'creator' or 'reviewer' for topic chats, null for member chats).
        Requires `canCreateMessageShortLink` (read access to the message's chat).
      tags:
        - ShortLinks
        - Chats
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the chat message to create a short link for.
      responses:
        '201':
          description: Short link created or existing short link returned.
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                    pattern: ^[a-zA-Z0-9]{8}$
                    description: The 8-character alphanumeric short link code.
                    example: Xk7mPq2R
                  shortUrl:
                    type: string
                    description: The complete short URL ready for sharing.
                    example: https://app.nurama.com/s/Xk7mPq2R
        '401':
          description: Unauthorized - Authentication required.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden - User does not have permission to access the chat message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
      x-request-source: joi
  /storage/chart/{resourceType}/{resourceId}:
    get:
      summary: Get storage chart data
      description: >
        Returns the average `sizeInBytes` of a resource's storage records per aggregation period between `startDate` and
        `endDate`, with missing periods filled from the previous value. Allowed when requesting your own user storage,
        or when you hold `canGet{ResourceType}Storage` (e.g. `canGetWorkspaceStorage`) on the resource.
      operationId: getStorageChart
      tags:
        - Storage
      parameters:
        - name: resourceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: resourceType
          in: path
          required: true
          schema:
            type: string
            enum:
              - user
              - workspace
              - project
              - chat
              - asset
        - name: startDate
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
          description: Range start as a UNIX epoch timestamp in milliseconds.
        - name: endDate
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
          description: Range end as a UNIX epoch timestamp in milliseconds.
        - name: aggregationPeriod
          in: query
          required: false
          schema:
            type: string
            default: day
            enum:
              - year
              - month
              - week
              - day
      responses:
        '200':
          description: Chart data retrieved successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChartDataArray'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          description: Error generating chart (`errorGeneratingChart`).
      security:
        - bearerAuth: []
      x-request-source: joi
  /storage/{resourceType}/{resourceId}:
    get:
      summary: Get storage record
      description: >
        Returns the most recent storage record for a resource. Allowed when requesting your own user storage, or when
        you hold `canGet{ResourceType}Storage` on the resource.
      operationId: getStorageRecord
      tags:
        - Storage
      parameters:
        - name: resourceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: resourceType
          in: path
          required: true
          schema:
            type: string
            enum:
              - user
              - workspace
              - project
              - chat
              - asset
      responses:
        '200':
          description: Storage record retrieved successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StorageRecord'
        '400':
          description: No storage record exists for the resource (`storageRecordDoesNotExist`).
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - bearerAuth: []
      x-request-source: joi
  /tags:
    post:
      summary: Create a new tag
      description: >-
        Creates a new project-scoped tag. Tags can then be applied to the project's assets, folders, submissions, boards
        and tasks via their `/tag` endpoints. Requires `canCreateTag` on `ownerResourceId`. Currently only
        `ownerResourceType: project` is accepted. Tag names must be unique per owner (400 `tagSlugExists`).
      tags:
        - Tags
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 50
                  description: The name of the tag
                  example: High Priority
                ownerResourceType:
                  type: string
                  enum:
                    - project
                  description: The type of resource that will own this tag (only `project` is supported today)
                  example: project
                ownerResourceId:
                  type: string
                  format: uuid
                  description: The ID of the resource that will own this tag
                  example: 65af4c7b22ac19c0b1648241
                color:
                  type: string
                  enum:
                    - '#37474F'
                    - '#FF5722'
                    - '#2962FF'
                    - '#33691E'
                    - '#00796B'
                    - '#455A64'
                    - '#2979FF'
                    - '#827717'
                    - '#7986CB'
                    - '#8E24AA'
                    - '#9575CD'
                    - '#BF360C'
                    - '#01579B'
                    - '#EF6C00'
                    - '#AA00FF'
                    - '#F44336'
                    - '#7C4DFF'
                    - '#E65100'
                    - '#8D6E63'
                    - '#283593'
                    - '#607D8B'
                    - '#009688'
                    - '#FF5252'
                    - '#03A9F4'
                    - '#C2185B'
                    - '#00ACC1'
                    - '#E91E63'
                    - '#5D4037'
                    - '#78909C'
                    - '#1E88E5'
                    - '#D500F9'
                    - '#7E57C2'
                    - '#5C6BC0'
                    - '#558B2F'
                    - '#2E7D32'
                    - '#F50057'
                    - '#004D40'
                    - '#0D47A1'
                    - '#C51162'
                    - '#D50000'
                    - '#6200EA'
                    - '#00BCD4'
                    - '#0277BD'
                  description: >-
                    Hex color code for the tag from the predefined color list. When omitted a color is assigned
                    automatically.
                  example: '#FF5722'
              required:
                - name
                - ownerResourceType
                - ownerResourceId
              additionalProperties: false
      responses:
        '201':
          description: Tag created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tag'
              examples:
                tagCreated:
                  summary: Successfully created tag
                  value:
                    id: 65af4c7b22ac19c0b1648555
                    name: High Priority
                    slug: high-priority
                    ownerResourceType: project
                    ownerResourceId: 65af4c7b22ac19c0b1648241
                    color: '#FF5722'
                    createdAt: 1678886400000
                    updatedAt: 1678886400000
        '400':
          description: Bad request - validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                nameRequired:
                  summary: Missing tag name
                  value:
                    type: validationError
                    code: 400
                    message: '"name" is required'
                invalidColor:
                  summary: Invalid color format
                  value:
                    type: validationError
                    code: 400
                    message: '"color" must be a valid hex color'
                tagExists:
                  summary: Tag name already exists
                  value:
                    type: tagSlugExists
                    code: 400
                    message: Tag with this name already exists for this resource.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/UnknownError'
      parameters: []
      x-request-source: joi
  /tags/{ownerResourceType}/{ownerResourceId}:
    get:
      summary: Get tags for a resource
      description: >-
        Retrieves all tags owned by a specific resource. Requires `canGetTags` on `ownerResourceId`. Currently only
        `ownerResourceType: project` is accepted.
      tags:
        - Tags
      security:
        - bearerAuth: []
      parameters:
        - name: ownerResourceType
          in: path
          required: true
          schema:
            type: string
            enum:
              - project
          description: The type of resource to get tags for (only `project` is supported today)
          example: project
        - name: ownerResourceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the resource to get tags for
          example: 65af4c7b22ac19c0b1648241
      responses:
        '200':
          description: Tags retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tags'
              examples:
                projectTags:
                  summary: Project tags
                  value:
                    - id: 65af4c7b22ac19c0b1648555
                      name: High Priority
                      slug: high-priority
                      ownerResourceType: project
                      ownerResourceId: 65af4c7b22ac19c0b1648241
                      color: '#FF5722'
                      createdAt: 1678886400000
                      updatedAt: 1678886400000
                    - id: 65af4c7b22ac19c0b1648556
                      name: In Review
                      slug: in-review
                      ownerResourceType: project
                      ownerResourceId: 65af4c7b22ac19c0b1648241
                      color: '#2962FF'
                      createdAt: 1678886500000
                      updatedAt: 1678886500000
                emptyTags:
                  summary: No tags found
                  value: []
        '400':
          description: Bad request - invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalidResourceType:
                  summary: Invalid resource type
                  value:
                    type: validationError
                    code: 400
                    message: '"ownerResourceType" must be [project]'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/UnknownError'
      x-request-source: joi
  /tags/{tagId}:
    put:
      summary: Update a tag
      description: >-
        Updates an existing tag's name and/or color. When the name is changed, the slug is automatically regenerated.
        Requires `canUpdateTag`.
      tags:
        - Tags
      security:
        - bearerAuth: []
      parameters:
        - name: tagId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the tag to update
          example: 65af4c7b22ac19c0b1648555
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 50
                  description: The new name for the tag
                  example: Updated Priority
                color:
                  type: string
                  enum:
                    - '#37474F'
                    - '#FF5722'
                    - '#2962FF'
                    - '#33691E'
                    - '#00796B'
                    - '#455A64'
                    - '#2979FF'
                    - '#827717'
                    - '#7986CB'
                    - '#8E24AA'
                    - '#9575CD'
                    - '#BF360C'
                    - '#01579B'
                    - '#EF6C00'
                    - '#AA00FF'
                    - '#F44336'
                    - '#7C4DFF'
                    - '#E65100'
                    - '#8D6E63'
                    - '#283593'
                    - '#607D8B'
                    - '#009688'
                    - '#FF5252'
                    - '#03A9F4'
                    - '#C2185B'
                    - '#00ACC1'
                    - '#E91E63'
                    - '#5D4037'
                    - '#78909C'
                    - '#1E88E5'
                    - '#D500F9'
                    - '#7E57C2'
                    - '#5C6BC0'
                    - '#558B2F'
                    - '#2E7D32'
                    - '#F50057'
                    - '#004D40'
                    - '#0D47A1'
                    - '#C51162'
                    - '#D50000'
                    - '#6200EA'
                    - '#00BCD4'
                    - '#0277BD'
                  description: The new hex color code for the tag from predefined color list
                  example: '#2962FF'
              additionalProperties: false
      responses:
        '200':
          description: Tag updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tag'
              examples:
                tagUpdated:
                  summary: Successfully updated tag
                  value:
                    id: 65af4c7b22ac19c0b1648555
                    name: Updated Priority
                    slug: updated-priority
                    ownerResourceType: project
                    ownerResourceId: 65af4c7b22ac19c0b1648241
                    color: '#2962FF'
                    createdAt: 1678886400000
                    updatedAt: '2023-03-15T12:10:00.000Z'
        '400':
          description: Bad request - validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalidColor:
                  summary: Invalid color format
                  value:
                    type: validationError
                    code: 400
                    message: '"color" must be a valid hex color'
                nameExists:
                  summary: Tag name already exists
                  value:
                    type: tagSlugExists
                    code: 400
                    message: Tag with this name already exists for this resource.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/UnknownError'
      x-request-source: joi
    delete:
      summary: Delete a tag
      description: Permanently deletes a tag. This action cannot be undone. Requires `canDeleteTag`.
      tags:
        - Tags
      security:
        - bearerAuth: []
      parameters:
        - name: tagId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ID of the tag to delete
          example: 65af4c7b22ac19c0b1648555
      responses:
        '200':
          description: Tag deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tag'
              examples:
                tagDeleted:
                  summary: Successfully deleted tag
                  value:
                    id: 65af4c7b22ac19c0b1648555
                    name: High Priority
                    slug: high-priority
                    ownerResourceType: project
                    ownerResourceId: 65af4c7b22ac19c0b1648241
                    color: '#FF5722'
                    createdAt: 1678886400000
                    updatedAt: 1678886400000
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/UnknownError'
      x-request-source: joi
  /tasks/{taskId}/relations:
    get:
      summary: List task relations
      description: >
        Returns a page of "Related To" entries for a task, populated with chat / message / asset / submission /
        public-link context and the user who created each relation. Rows whose chat the caller cannot read are filtered
        out (`totalResults` reflects the filtered page). Requires read access to the task's board.
      tags:
        - Task Relations
      security:
        - bearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Results per page
      responses:
        '200':
          description: Paginated list of relations
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedTaskRelations'
        '403':
          description: Workspace lacks the `boards` capability (`capabilityNotAvailable`) or the user cannot read the task's board
      x-request-source: joi
    post:
      summary: Relate a chat or chat message to a task
      description: |
        Creates a relation between a task and either a Chat or a single ChatMessage. The task must be on a board.
        Visibility rules are enforced server-side:

          - Submission and public chats can attach to any task.
          - Creator chats only attach to tasks on creator-visibility boards.
          - Reviewer chats only attach to tasks on reviewer-visibility boards.

        For chat messages the visibility is derived from the message's parent chat.
        Requires update access on the task's board plus read access to the chat / message being attached.
      tags:
        - Task Relations
      security:
        - bearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                resourceId:
                  type: string
                  format: uuid
                  description: ID of the chat or chat message to relate
                resourceType:
                  type: string
                  enum:
                    - chat
                    - chatMessage
              required:
                - resourceId
                - resourceType
              additionalProperties: false
              example:
                resourceId: 0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b
                resourceType: chatMessage
      responses:
        '201':
          description: Created relation (unpopulated)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskRelationRecord'
        '400':
          description: >-
            Validation error, invalid resource type (`invalidRelatedResourceType`) or the task is not on a board
            (`invalidTask`)
        '403':
          description: >-
            Chat visibility is incompatible with the board (`relationVisibilityMismatch`), or the workspace lacks the
            `boards` capability
        '404':
          description: Chat / message not found (`relatedResourceNotFound`) or board not found (`boardNotFound`)
        '409':
          description: Relation already exists (`relationAlreadyExists`)
      x-request-source: joi
  /tasks/{taskId}/relations/{relationId}:
    delete:
      summary: Remove a task relation
      description: Deletes the relation and notifies the board's project channels. Requires update access on the task's board.
      tags:
        - Task Relations
      security:
        - bearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: relationId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Relation removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Relation removed
        '403':
          description: >-
            Workspace lacks the `boards` capability (`capabilityNotAvailable`) or the user cannot update the task's
            board
        '404':
          description: Relation not found (`relatedResourceNotFound`)
      x-request-source: joi
  /chats/{chatId}/relations:
    get:
      summary: List task relations for a chat
      description: >-
        Returns a page of relations whose resource is this chat, populated like `GET /tasks/{taskId}/relations`.
        Requires read access to the chat.
      tags:
        - Task Relations
      security:
        - bearerAuth: []
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Results per page
      responses:
        '200':
          description: Paginated list of relations referencing this chat
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedTaskRelations'
        '403':
          description: Workspace lacks the `boards` capability (`capabilityNotAvailable`) or the user cannot read the chat
        '404':
          description: Chat not found
      x-request-source: joi
  /chat-messages/{messageId}/relations:
    get:
      summary: List task relations for a chat message
      description: >-
        Returns a page of relations whose resource is this chat message, populated like `GET /tasks/{taskId}/relations`.
        Requires read access to the message's chat.
      tags:
        - Task Relations
      security:
        - bearerAuth: []
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Results per page
      responses:
        '200':
          description: Paginated list of relations referencing this chat message
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedTaskRelations'
        '403':
          description: Workspace lacks the `boards` capability (`capabilityNotAvailable`) or the user cannot read the message
        '404':
          description: Message not found
      x-request-source: joi
  /tasks:
    get:
      summary: Get paginated list of tasks assigned to a user.
      description: >-
        Returns tasks where `assignedToId` is the authenticated user. Supports index (default) or cursor pagination; the
        cursor-only and index-only parameters are mutually exclusive with the other `paginate` mode.
      tags:
        - Tasks
      security:
        - bearerAuth: []
      parameters:
        - name: paginate
          in: query
          required: false
          schema:
            type: string
            default: index
            enum:
              - cursor
              - index
          description: Pagination type to use
        - name: projectId
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: creatorId
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: relatedToId
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: visibility
          in: query
          required: false
          schema:
            type: string
            enum:
              - creator
              - reviewer
        - name: acknowledged
          in: query
          required: false
          schema:
            type: boolean
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
              acknowledged:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
          style: deepObject
          explode: true
          description: Sort options
        - name: limit
          in: query
          required: false
          schema:
            type: number
            maximum: 100
            default: 20
          description: Limit number of results
        - name: page
          in: query
          required: false
          schema:
            type: number
            x-conditionally-required: true
          description: Page number (only for index pagination)
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            x-conditionally-required: true
          description: Cursor for pagination (only accepted with `paginate=cursor`).
        - name: paginateReverse
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, paginates in reverse order (only accepted with `paginate=cursor`).
        - name: includeCounts
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes count information (only accepted with `paginate=cursor`).
        - name: includeCursorRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the cursor record in results (only accepted with `paginate=cursor`).
        - name: startAt
          in: query
          required: false
          schema:
            type: string
            format: uuid
            x-conditionally-required: true
          description: ID of the record to start pagination from (only accepted with `paginate=cursor`).
        - name: includeStartAtRecord
          in: query
          required: false
          schema:
            type: boolean
            x-conditionally-required: true
          description: If true, includes the startAt record in the results (only accepted with `paginate=cursor`).
        - name: createdBefore
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            default: null
            nullable: true
            x-conditionally-required: true
          description: Get tasks created before this timestamp (only for index pagination)
        - name: createdAfter
          in: query
          required: false
          schema:
            oneOf:
              - type: string
                format: date-time
              - type: string
            default: null
            nullable: true
            x-conditionally-required: true
          description: Get tasks created after this timestamp (only for index pagination)
      responses:
        '200':
          description: Successfully retrieved tasks.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Tasks'
                  - $ref: '#/components/schemas/CursorPaginatedTasks'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
      x-request-source: joi
  /tasks/{taskId}:
    put:
      summary: Update status of task.
      description: >-
        Requires `canUpdateOwnTaskStatus` (the task must be assigned to the caller, or the caller must hold an elevated
        task permission).
      tags:
        - Tasks
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum:
                    - pending
                    - inProgress
                    - complete
                    - closed
              required:
                - status
              additionalProperties: false
      responses:
        '200':
          description: Successfully updated task status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/TaskNotFound'
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the task.
      x-request-source: joi
    delete:
      summary: Delete a task
      description: |
        Permanently removes a single task. Requires `canRemove{Creator|Reviewer}BoardTask` for
        at least one visibility tier of the task's parent board, plus the `boards` workspace
        capability. Logs a `taskDeleted` event.
      tags:
        - Tasks
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Task deleted (also returned when the task no longer exists).
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the task.
      x-request-source: joi
  /tasks/bulk-create:
    post:
      summary: Create several board tasks in one request
      description: |
        Single round-trip replacement for creating tasks one at a time. Every draft is
        created in order; per-draft failures are reported inline (`status: 'error'`) and do
        not abort the batch, so the response is always 200 once validation passes.

        Any logged-in user may call this; the per-board `canCreate{Creator|Reviewer}BoardTask`
        permission is checked for each draft (`no_permission`), as is that the board is
        active and belongs to `projectId` (`board_not_found`). Requires the `boards`
        workspace capability.

        Each created task gets the same side effects as `POST /boards/{boardId}/tasks`
        (event log, notification, `created` task event, topic chat) and, when `tagIds` is
        supplied, the tags are applied inline. When `announce` is provided and at least one
        task was created, a single assistant reply carrying every new task is posted in the
        source chat as a reply to `messageId`; announce failures never fail the request.
      tags:
        - Tasks
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                projectId:
                  type: string
                  format: uuid
                  description: Project every target board must belong to.
                tasks:
                  type: array
                  items:
                    type: object
                    properties:
                      clientId:
                        type: string
                        description: Caller-chosen ID echoed back in the matching result so drafts can be mapped to outcomes.
                      boardId:
                        type: string
                        format: uuid
                      columnId:
                        type: string
                        format: uuid
                        description: Defaults to the board's default column.
                      subject:
                        type: string
                        maxLength: 500
                      description:
                        type: string
                        maxLength: 20000
                        nullable: true
                      assignedToId:
                        type: string
                        format: uuid
                      tagIds:
                        type: array
                        items:
                          type: string
                          format: uuid
                    required:
                      - clientId
                      - boardId
                      - subject
                    additionalProperties: false
                  minItems: 1
                  maxItems: 50
                announce:
                  type: object
                  properties:
                    chatId:
                      type: string
                      format: uuid
                    messageId:
                      type: string
                      format: uuid
                  required:
                    - chatId
                    - messageId
                  additionalProperties: false
              required:
                - projectId
                - tasks
              additionalProperties: false
      responses:
        '200':
          description: One result per submitted draft, in submission order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        clientId:
                          type: string
                        status:
                          type: string
                          enum:
                            - created
                            - error
                        task:
                          $ref: '#/components/schemas/Task'
                        error:
                          type: object
                          description: Present when `status` is `error`.
                          properties:
                            code:
                              type: string
                              description: >-
                                `missing_board_id`, `missing_subject`, `board_not_found`, `no_permission`, or the
                                failing ApiError name (e.g. `invalidAssignee`, `reviewerCannotContributeHere`).
                            message:
                              type: string
                  announce:
                    type: object
                    description: Present only when `announce` was supplied and at least one task was created.
                    properties:
                      posted:
                        type: boolean
                      messageId:
                        type: string
                        format: uuid
                        nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/CapabilityNotAvailable'
      parameters: []
      x-request-source: joi
  /tasks/acknowledge/{taskId}:
    put:
      summary: Toggle acknowledgement of a task.
      description: Flips the task's `acknowledged` flag. Requires `canUpdateOwnTaskStatus`.
      tags:
        - Tasks
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully toggled task acknowledgement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/TaskNotFound'
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the task.
      x-request-source: joi
  /tasks/{taskId}/follow:
    put:
      summary: Follow a task
      description: Add the current user to the task's followers list. Idempotent — following again has no effect.
      tags:
        - Tasks
      security:
        - bearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Task with updated followers list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /tasks/{taskId}/unfollow:
    put:
      summary: Unfollow a task
      description: Remove the current user from the task's followers list.
      tags:
        - Tasks
      security:
        - bearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Task with updated followers list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /tasks/{taskId}/events:
    get:
      summary: Get task event log
      description: >
        Returns a paginated activity log for a task. Each event includes the actor (public user with avatar) and a
        structured `detail` object with the context needed to render a human-readable message.

        Event types: created, addedToBoard, moved, assigned, unassigned, updated, removedFromBoard, linked, unlinked,
        followed, unfollowed.
      tags:
        - Tasks
      security:
        - bearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            default:
              createdAt: -1
          style: deepObject
          explode: true
          description: Sort by createdAt
        - name: eventType
          in: query
          required: false
          schema:
            type: string
            maxLength: 50
          description: >-
            Filter by event type (exact match). Validation accepts any string up to 50 characters; known values are
            created, addedToBoard, moved, assigned, unassigned, updated, removedFromBoard, linked, unlinked, followed,
            unfollowed.
      responses:
        '200':
          description: Paginated list of task events.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedTaskEvents'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /tasks/{taskId}/tag:
    put:
      summary: Tag a task
      description: Add a project tag to a task. Idempotent — tagging again has no effect.
      tags:
        - Tasks
      security:
        - bearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
                  description: ID of the project tag to add
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Task with updated tags.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /tasks/{taskId}/untag:
    put:
      summary: Untag a task
      description: Remove a project tag from a task.
      tags:
        - Tasks
      security:
        - bearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tagId:
                  type: string
                  format: uuid
              required:
                - tagId
              additionalProperties: false
      responses:
        '200':
          description: Task with updated tags.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-request-source: joi
  /tasks/unacknowledged/{projectId}:
    get:
      summary: Get count of unacknowledged tasks for a project.
      description: Counts the authenticated user's unacknowledged tasks in the project, split by visibility.
      tags:
        - Tasks
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully retrieved unacknowledged task counts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  total:
                    type: integer
                  creator:
                    type: integer
                  reviewer:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the project.
      x-request-source: joi
  /tasks/acknowledge-all/{projectId}:
    put:
      summary: Acknowledge all of the current user's unacknowledged mention-tasks in a project.
      description: >
        Sets `acknowledged: true` on every unacknowledged mention-task assigned to the authenticated user in the
        project. Idempotent — already-acknowledged tasks are left as-is and never re-counted. Scoped to the caller's own
        tasks; other users' tasks are untouched.
      tags:
        - Tasks
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully acknowledged the user's outstanding mention-tasks.
          content:
            application/json:
              schema:
                type: object
                properties:
                  acknowledged:
                    type: integer
                    description: Number of tasks acknowledged by this call.
                  unacknowledgedCount:
                    type: object
                    description: The user's remaining unacknowledged counts for the project (all zero after this call).
                    properties:
                      total:
                        type: integer
                      creator:
                        type: integer
                      reviewer:
                        type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the project.
      x-request-source: joi
  /users:
    get:
      summary: Get logged in user's details.
      tags:
        - Users
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully retrieved logged in user details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /users/{userId}:
    get:
      summary: Get public information about a user with shared membership.
      description: >-
        Returns the public profile of a user who shares at least one workspace/project membership with the caller (or
        the caller's own public profile).
      tags:
        - Users
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully retrieved public user information.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicUser'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/UserNotFound'
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: User ID of the target user.
      x-request-source: joi
  /users/preferences:
    patch:
      summary: Update the logged-in user's preferences.
      description: |
        Patch-merges the supplied fields into the user's `preferences` JSON.
        Omitted keys are left untouched. Used for notification settings, sound
        preferences, hide flags, and onboarding-Todo dismissals. Unknown keys
        (at any level) are rejected with 400.

        `dismissed.*` groups per-user UX opt-outs (todos, workspace-setup
        prompts, etc). The `dismissed` object is merged one level deep, so
        writing one inner key preserves the others. For todos, unknown ids and
        ids of Todos that are not dismissible (such as `verifyEmail`) are
        ignored.
      tags:
        - Users
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                hide:
                  type: array
                  items:
                    enum:
                      - feedHint
                      - imagesHint
                      - theaterHint
                      - reviewerHint
                      - deckHint
                      - videoHint
                  description: In-app hints the user has hidden.
                emailNotifications:
                  type: object
                  properties:
                    sendEmailNotifications:
                      type: boolean
                    newMention:
                      type: boolean
                    newMentionActiveInterval:
                      type: integer
                      minimum: 0
                    newFollowing:
                      type: boolean
                    newFollowingActiveInterval:
                      type: integer
                      minimum: 0
                    newFollowingEmailCooldown:
                      type: integer
                      minimum: 0
                    newHighlight:
                      type: boolean
                    newHighlightActiveInterval:
                      type: integer
                      minimum: 0
                    dailySummary:
                      type: boolean
                    publishedAsset:
                      type: boolean
                  additionalProperties: false
                systemNotifications:
                  type: object
                  properties:
                    showSystemNotifications:
                      type: boolean
                    showDesktopAppNotifications:
                      type: boolean
                    showWebAppNotifications:
                      type: boolean
                    showMobileAppNotifications:
                      type: boolean
                    mentions:
                      type: boolean
                    alerts:
                      type: boolean
                    privateMessages:
                      type: boolean
                  additionalProperties: false
                sounds:
                  type: object
                  properties:
                    playSounds:
                      type: boolean
                    playDesktopAppSounds:
                      type: boolean
                    playWebAppSounds:
                      type: boolean
                    playMobileAppSounds:
                      type: boolean
                    mentions:
                      type: boolean
                    alerts:
                      type: boolean
                    privateMessages:
                      type: boolean
                  additionalProperties: false
                dismissed:
                  type: object
                  properties:
                    todos:
                      type: array
                      items:
                        type: string
                        maxLength: 64
                        example: enable2fa
                      description: Ids of onboarding Todos the user has opted out of (see `GET /users/todos`).
                  additionalProperties: false
                  description: >-
                    Per-user UX opt-outs. One-level-deep server-side merge keeps siblings intact when only one key is
                    sent. Only `todos` is currently accepted.
                dailyTips:
                  type: object
                  properties:
                    disabled:
                      type: boolean
                      description: When true, the daily-tips carousel never shows.
                    lastShownDate:
                      type: string
                      pattern: ^\d{4}-\d{2}-\d{2}$
                      description: Local `YYYY-MM-DD` the carousel was last shown; gates the once-per-day rule.
                      example: '2026-09-17'
                  additionalProperties: false
                  description: |
                    Daily-tips carousel state. Merged one level deep, so writing one key preserves the other.
                featureIntros:
                  type: object
                  additionalProperties:
                    type: array
                    items:
                      type: string
                      maxLength: 64
                  description: >
                    Per-project feature-intro tutorials the user has seen (Highlights / Submissions / Public / Boards),
                    keyed by project id. Merged one level deep, so writing one project's list preserves the others.
                dateFormat:
                  type: string
                  enum:
                    - european
                    - american
                    - iso
                  description: >
                    Preferred date display format. Seeded at registration from the country the registration request came
                    from: a month-first country (e.g. the US) yields `american`, everything else `european`. European is
                    the fallback whenever the value is unset or the country cannot be determined. Users can override it
                    (e.g. to `iso`, YYYY-MM-DD) from their profile.
                  example: european
              additionalProperties: false
      responses:
        '200':
          description: Preferences updated. Returns the full user object with merged preferences.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
      parameters: []
      x-request-source: joi
  /users/avatar:
    post:
      tags:
        - Users
      summary: Create or update an avatar.
      description: >-
        Creates the avatar asset and returns signed upload links for it. Any existing avatar asset is marked
        `pendingDelete`. POST and PUT are identical.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedIdNameChecksum2'
      responses:
        '200':
          description: >-
            Avatar asset and upload links created/updated successfully. The upload-link entry is returned at the top
            level together with the updated `user`.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/AssetAndSignedLink'
                  - type: object
                    properties:
                      user:
                        $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          description: '`assetUploadError` — the avatar asset could not be created.'
      parameters: []
      x-request-source: joi
    put:
      tags:
        - Users
      summary: Create or update an avatar.
      description: >-
        Creates the avatar asset and returns signed upload links for it. Any existing avatar asset is marked
        `pendingDelete`. POST and PUT are identical.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedIdNameChecksum2'
      responses:
        '200':
          description: >-
            Avatar asset and upload links created/updated successfully. The upload-link entry is returned at the top
            level together with the updated `user`.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/AssetAndSignedLink'
                  - type: object
                    properties:
                      user:
                        $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          description: '`assetUploadError` — the avatar asset could not be created.'
      parameters: []
      x-request-source: joi
  /version/commit:
    get:
      summary: Get the latest git commit hash
      tags:
        - Version
      description: >
        Returns the latest git commit hash and build timestamp of the deployed application. Public — no authentication
        required. The commit is injected during the build process, so accurate values are only available on deployed
        builds.
      responses:
        '200':
          description: Successfully retrieved the latest git commit hash.
          content:
            application/json:
              schema:
                type: object
                properties:
                  buildCommit:
                    type: string
                    example: ab12cd34ef56gh78ij90kl [timeStamp]
                    description: The git commit hash of the current build.
  /workspaces/{workspaceId}/webhooks:
    post:
      summary: Create a webhook subscription
      description: >-
        Registers a delivery URL for the given events and returns the new subscription together with its `signingSecret`
        — the only time the secret is returned other than on rotate. The URL must be `http(s)`, ≤ 2048 chars, and must
        not point at a private / loopback / link-local address (literal IPs are rejected outright; hostnames are
        DNS-resolved and checked). Deployments that require HTTPS reject `http://` URLs except for `localhost`.
      tags:
        - Webhook Subscriptions
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 80
                  description: Customer-facing label. Whitespace-only names are rejected.
                url:
                  type: string
                  format: uri
                  maxLength: 2048
                  description: Delivery endpoint. `http` or `https`; public hosts only.
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - task.created
                      - task.updated
                      - task.deleted
                      - chat.message.created
                      - asset.published
                      - webhook.test
                  minItems: 1
                  description: Wire event names to subscribe to. Duplicates are removed.
                expiresAt:
                  type: string
                  format: date-time
                  nullable: true
                  description: Optional ISO timestamp in the future. Null / omitted means no expiry.
              required:
                - name
                - url
                - events
              additionalProperties: false
      responses:
        '201':
          description: Subscription created. Store `signingSecret` now — it cannot be retrieved later.
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscription:
                    $ref: '#/components/schemas/WebhookSubscription'
                  signingSecret:
                    type: string
                    description: HMAC signing secret (plaintext). Returned once.
        '400':
          description: >-
            Validation error, or one of `webhookUrlInvalid`, `webhookUrlInsecure`, `webhookUrlBlocked` (resolves to a
            private address), `webhookUrlUnresolvable`, `webhookNameRequired`, `webhookEventsInvalid`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Caller lacks `canManageWebhooks`, or the workspace has reached the cap (`webhookLimitReached`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
      x-request-source: joi
    get:
      summary: List the workspace's webhook subscriptions
      description: Newest first. Secrets are never included.
      tags:
        - Webhook Subscriptions
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Subscriptions in the workspace.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebhookSubscription'
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
      x-request-source: joi
  /workspaces/{workspaceId}/webhooks/{webhookId}:
    get:
      summary: Get a webhook subscription
      tags:
        - Webhook Subscriptions
      security:
        - bearerAuth: []
      responses:
        '200':
          description: The subscription (sanitised).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscription'
        '404':
          description: No subscription with this id in this workspace.
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Webhook subscription ID.
      x-request-source: joi
    patch:
      summary: Update a webhook subscription
      description: >-
        Partial update; at least one field is required. `status` accepts only the admin-controllable transitions
        `active` and `paused` — setting `active` on a `failedOut` subscription re-enables it and clears `failedOutAt` /
        `failedOutReason`. Pass `expiresAt: null` to clear an expiry. URL and event changes go through the same checks
        as create.
      tags:
        - Webhook Subscriptions
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 80
                url:
                  type: string
                  format: uri
                  maxLength: 2048
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - task.created
                      - task.updated
                      - task.deleted
                      - chat.message.created
                      - asset.published
                      - webhook.test
                  minItems: 1
                status:
                  type: string
                  enum:
                    - active
                    - paused
                expiresAt:
                  type: string
                  format: date-time
                  nullable: true
                  description: Future ISO timestamp to set / extend, or null to clear.
              additionalProperties: false
              minProperties: 1
      responses:
        '200':
          description: Updated subscription.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscription'
        '400':
          description: >-
            Validation error, or `webhookUpdateEmpty`, `webhookNameRequired`, `webhookStatusInvalid`,
            `webhookEventsInvalid`, `webhookUrlInvalid`, `webhookUrlInsecure`, `webhookUrlBlocked`,
            `webhookUrlUnresolvable`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No subscription with this id in this workspace.
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Webhook subscription ID.
      x-request-source: joi
    delete:
      summary: Delete a webhook subscription
      description: Permanently removes the subscription and its delivery attempts.
      tags:
        - Webhook Subscriptions
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Deleted.
        '404':
          description: No subscription with this id in this workspace.
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Webhook subscription ID.
      x-request-source: joi
  /workspaces/{workspaceId}/webhooks/{webhookId}/rotate-secret:
    post:
      summary: Rotate the signing secret
      description: >-
        Generates a new HMAC signing secret and returns it once. Deliveries signed after this call use the new secret
        immediately; update the receiver's verification config first if you need zero-downtime rotation.
      tags:
        - Webhook Subscriptions
      security:
        - bearerAuth: []
      responses:
        '200':
          description: New secret issued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscription:
                    $ref: '#/components/schemas/WebhookSubscription'
                  signingSecret:
                    type: string
                    description: The new plaintext signing secret. Returned once.
        '404':
          description: No subscription with this id in this workspace.
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Webhook subscription ID.
      x-request-source: joi
  /workspaces/{workspaceId}/webhooks/{webhookId}/test:
    post:
      summary: Queue a test delivery
      description: >-
        Fires a synthetic `webhook.test` event through the normal delivery pipeline. The subscription does not need
        `webhook.test` in its `events` list. The response only confirms the event was queued — inspect the delivery log
        (`GET .../deliveries`) for the outcome.
      tags:
        - Webhook Subscriptions
      security:
        - bearerAuth: []
      responses:
        '202':
          description: Test delivery queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  queued:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: Test webhook delivery queued. Check the delivery log in a moment.
        '404':
          description: No subscription with this id in this workspace.
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Webhook subscription ID.
      x-request-source: joi
  /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries:
    get:
      summary: List delivery attempts
      description: >-
        Cursor-paginated delivery log, newest first. Each retry of a notification is its own attempt. Pass the returned
        `nextCursor` back as `cursor` to fetch the next page.
      tags:
        - Webhook Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Webhook subscription ID.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: >-
            Opaque cursor from a previous response's `nextCursor` (an ISO `createdAt` timestamp; only older attempts are
            returned).
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - pending
              - inflight
              - succeeded
              - failed
              - dlq
      responses:
        '200':
          description: Page of attempts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookAttempt'
                  nextCursor:
                    type: string
                    nullable: true
                    description: Cursor for the next page, or null when there are no more attempts.
        '404':
          description: No subscription with this id in this workspace.
      x-request-source: joi
  /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries/{attemptId}/replay:
    post:
      summary: Replay a delivery
      description: >-
        Re-queues the attempt's notification for delivery. A NEW attempt is created (the original is kept for the audit
        trail) and the notification is queued for delivery again.
      tags:
        - Webhook Subscriptions
      security:
        - bearerAuth: []
      responses:
        '202':
          description: Replay queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  attempt:
                    $ref: '#/components/schemas/WebhookAttempt'
        '404':
          description: No subscription with this id in this workspace, or no attempt with this id on the subscription.
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Webhook subscription ID.
        - name: attemptId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of an existing attempt on this subscription.
      x-request-source: joi
  /workspaces:
    post:
      summary: Create a new workspace
      tags:
        - Workspaces
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 100
                description:
                  type: string
                  maxLength: 350
                logo:
                  type: string
                  description: URL to logo
                icon:
                  type: string
                  description: URL to icon
                color:
                  type: string
                  enum:
                    - '#37474F'
                    - '#FF5722'
                    - '#2962FF'
                    - '#33691E'
                    - '#00796B'
                    - '#455A64'
                    - '#2979FF'
                    - '#827717'
                    - '#7986CB'
                    - '#8E24AA'
                    - '#9575CD'
                    - '#BF360C'
                    - '#01579B'
                    - '#EF6C00'
                    - '#AA00FF'
                    - '#F44336'
                    - '#7C4DFF'
                    - '#E65100'
                    - '#8D6E63'
                    - '#283593'
                    - '#607D8B'
                    - '#009688'
                    - '#FF5252'
                    - '#03A9F4'
                    - '#C2185B'
                    - '#00ACC1'
                    - '#E91E63'
                    - '#5D4037'
                    - '#78909C'
                    - '#1E88E5'
                    - '#D500F9'
                    - '#7E57C2'
                    - '#5C6BC0'
                    - '#558B2F'
                    - '#2E7D32'
                    - '#F50057'
                    - '#004D40'
                    - '#0D47A1'
                    - '#C51162'
                    - '#D50000'
                    - '#6200EA'
                    - '#00BCD4'
                    - '#0277BD'
                  description: Hex colour code from the platform's approved palette (e.g. `#37474F`).
                  example: '#37474F'
              additionalProperties: false
      responses:
        '201':
          description: >-
            Workspace created successfully. The response is the workspace row with `roles: ['workspaceOwner']` stamped
            on for the caller. `capabilities` is not included on create (there is no subscription yet) — fetch the
            workspace to get it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceWithRoles'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      parameters: []
      x-request-source: joi
    get:
      summary: Get all workspaces for the authenticated user
      description: >-
        Returns every workspace the caller holds a membership in, each stamped with the caller's `roles` and the
        workspace's `capabilities`. Sort with the `sort` query parameter (deep-object style, e.g.
        `?sort[name]=1&sort[createdAt]=-1`). A `sort` object in the request body is still accepted as a deprecated alias
        for one release.
      tags:
        - Workspaces
      security:
        - bearerAuth: []
      parameters:
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            description: Sort directions per field (1 ascending, -1 descending).
          style: deepObject
          explode: true
      responses:
        '200':
          description: Successfully retrieved user's workspaces.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WorkspaceWithRoles'
        '401':
          $ref: '#/components/responses/Unauthorized'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                sort:
                  type: object
                  properties:
                    id:
                      type: number
                      enum:
                        - 1
                        - -1
                    name:
                      type: number
                      enum:
                        - 1
                        - -1
                    createdAt:
                      type: number
                      enum:
                        - 1
                        - -1
                    updatedAt:
                      type: number
                      enum:
                        - 1
                        - -1
                  additionalProperties: false
              additionalProperties: false
      x-request-source: joi
  /workspaces/{workspaceId}:
    get:
      summary: Get details of a specific workspace
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
      tags:
        - Workspaces
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully retrieved workspace details, including the caller's `roles` and the workspace's `capabilities`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceWithRoles'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/WorkspaceNotFound'
      x-request-source: joi
    put:
      summary: Update details of a specific workspace
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
      tags:
        - Workspaces
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 100
                description:
                  type: string
                  maxLength: 350
                updateSlug:
                  type: boolean
                  default: false
                  description: >-
                    Determines if the slug will be regenerated or not. Meaning you could change the name but keep the
                    old slug as it may be used in static links.
                color:
                  type: string
                  enum:
                    - '#37474F'
                    - '#FF5722'
                    - '#2962FF'
                    - '#33691E'
                    - '#00796B'
                    - '#455A64'
                    - '#2979FF'
                    - '#827717'
                    - '#7986CB'
                    - '#8E24AA'
                    - '#9575CD'
                    - '#BF360C'
                    - '#01579B'
                    - '#EF6C00'
                    - '#AA00FF'
                    - '#F44336'
                    - '#7C4DFF'
                    - '#E65100'
                    - '#8D6E63'
                    - '#283593'
                    - '#607D8B'
                    - '#009688'
                    - '#FF5252'
                    - '#03A9F4'
                    - '#C2185B'
                    - '#00ACC1'
                    - '#E91E63'
                    - '#5D4037'
                    - '#78909C'
                    - '#1E88E5'
                    - '#D500F9'
                    - '#7E57C2'
                    - '#5C6BC0'
                    - '#558B2F'
                    - '#2E7D32'
                    - '#F50057'
                    - '#004D40'
                    - '#0D47A1'
                    - '#C51162'
                    - '#D50000'
                    - '#6200EA'
                    - '#00BCD4'
                    - '#0277BD'
                  description: Hex colour code from the platform's approved palette (e.g. `#37474F`).
                  example: '#37474F'
              additionalProperties: false
              description: All fields optional. `logo` / `icon` are NOT accepted here — use the `/logo` and `/icon` endpoints.
      responses:
        '200':
          description: Successfully updated workspace details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workspace'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/WorkspaceNotFound'
      x-request-source: joi
    delete:
      summary: Change a workspace's status to 'pendingDelete' for later cleanup.
      description: >
        Requires `canDeleteWorkspace`, which only the workspace OWNER holds — not `canUpdateWorkspace`, which admins
        also hold. Deleting the workspace is the one way an admin could take it away from its owner, so it costs the
        owner-only right.
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
      tags:
        - Workspaces
      security:
        - bearerAuth: []
      responses:
        '200':
          description: 'Workspace successfully marked for deletion. Returns the updated workspace (`status: pendingDelete`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workspace'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/WorkspaceNotFound'
      x-request-source: joi
  /workspaces/{workspaceId}/projects:
    get:
      summary: Get projects within a specific workspace
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
        - name: sort
          in: query
          required: false
          schema:
            type: object
            properties:
              id:
                type: number
                enum:
                  - 1
                  - -1
              name:
                type: number
                enum:
                  - 1
                  - -1
              createdAt:
                type: number
                enum:
                  - 1
                  - -1
              updatedAt:
                type: number
                enum:
                  - 1
                  - -1
            additionalProperties: false
            description: Sort directions per field (1 ascending, -1 descending).
          style: deepObject
          explode: true
      tags:
        - Workspaces
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successfully retrieved projects within the workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Projects'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/WorkspaceNotFound'
      x-request-source: joi
  /workspaces/{workspaceId}/logo:
    post:
      deprecated: true
      tags:
        - Workspaces
      summary: Create or update workspace logo. (deprecated alias; use PUT)
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the workspace
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedIdNameChecksum3'
      responses:
        '200':
          description: Workspace logo asset and upload links created/updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceImageUploadResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/WorkspaceNotFound'
      x-request-source: joi
    put:
      tags:
        - Workspaces
      summary: Create or update workspace logo.
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the workspace
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedIdNameChecksum3'
      responses:
        '200':
          description: Workspace logo asset and upload links created/updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceImageUploadResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/WorkspaceNotFound'
      x-request-source: joi
  /workspaces/{workspaceId}/icon:
    post:
      deprecated: true
      tags:
        - Workspaces
      summary: Create or update workspace icon. (deprecated alias; use PUT)
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the workspace
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedIdNameChecksum3'
      responses:
        '200':
          description: Workspace icon asset and upload links created/updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceImageUploadResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/WorkspaceNotFound'
      x-request-source: joi
    put:
      tags:
        - Workspaces
      summary: Create or update workspace icon.
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique ID of the workspace
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SharedIdNameChecksum3'
      responses:
        '200':
          description: Workspace icon asset and upload links created/updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceImageUploadResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/WorkspaceNotFound'
      x-request-source: joi
  /workspaces/{workspaceId}/settings/{name}:
    put:
      tags:
        - Workspaces
      summary: Update a single workspace setting.
      description: >-
        Sets one key of the workspace's `settings` JSON column. The `name` path parameter selects the setting and
        determines the type `value` must have (booleans for the toggles, bounded integers for the credit budgets,
        strings for the pre-prompts, `{ toolName: boolean|null }` maps for `aiChatTools` / `aiAssistTools`). Requires
        `canManageWorkspaceSettings` on the workspace.


        Setting-specific behaviour:

        - `autoTopUpEnabled: true` is refused with `autoTopUpRequiresPriorTopUp`
          until the workspace has made at least one manual credit top-up.

        - Flipping `boardsEnabled` true→false schedules every board in the
          workspace for deletion after a 48h grace window; flipping it back
          within the window restores them.
      security:
        - bearerAuth: []
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the workspace.
        - name: name
          in: path
          required: true
          schema:
            type: string
            enum:
              - aiAddOnEnabled
              - allowAiFeatures
              - aiPolishEnabled
              - aiComposeEnabled
              - aiChatEnabled
              - aiAssistEnabled
              - aiAssistFollowUpWindow
              - aiTaskGenerationEnabled
              - aiImageRevisionEnabled
              - aiPolishAllowReviewer
              - aiComposeAllowReviewer
              - aiChatAllowReviewer
              - aiAssistAllowReviewer
              - aiTaskGenerationAllowReviewer
              - aiImageRevisionAllowReviewer
              - aiCustomPreprompt
              - aiTaskGenerationPreprompt
              - dailyUserAiSpendBudget
              - autoTopUpEnabled
              - autoTopUpThresholdCredits
              - autoTopUpAmountCredits
              - autoTopUpMonthlyLimitCredits
              - aiChatTools
              - aiAssistTools
              - boardsEnabled
              - convosEnabled
          description: The setting to update.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                value:
                  description: >-
                    New value. Type depends on `name`: `*Enabled` / `allowAiFeatures` / `*AllowReviewer` — boolean;
                    `aiAssistFollowUpWindow` — integer 0..30; `aiCustomPreprompt` / `aiTaskGenerationPreprompt` — string
                    ≤ 8000 chars ('' clears); `dailyUserAiSpendBudget` — integer 0..1000000 (0 disables the cap);
                    `autoTopUp*Credits` — integer ≥ 0; `aiChatTools` / `aiAssistTools` — object mapping
                    `^[a-z][a-z0-9_]*$` tool names to boolean or null.
              required:
                - value
              additionalProperties: false
      responses:
        '200':
          description: Setting updated. Returns the full updated workspace row (including `settings`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workspace'
        '400':
          description: >-
            Validation error (unknown `name`, or `value` has the wrong type/range for it — `validationError`),
            `workspaceInvalidSetting`, or `autoTopUpRequiresPriorTopUp`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/WorkspaceNotFound'
      x-request-source: joi
components:
  schemas:
    BaseAnnotation:
      type: object
      properties:
        color:
          type: string
          description: Hex color code (e.g.,
          default: '#9b9b9b'
        frame:
          type: number
          default: 0
          description: Video frame number (for video annotations)
        timestamp:
          type: number
          description: Legacy single timestamp in milliseconds (for backward compatibility)
        startTimestamp:
          type: number
          description: Range start timestamp or single point timestamp in milliseconds
        endTimestamp:
          type: number
          description: Range end timestamp in milliseconds (optional, creates range if provided)
        left:
          type: number
          minimum: 0
          maximum: 100
          description: X position as percentage of media width
        top:
          type: number
          minimum: 0
          maximum: 100
          description: Y position as percentage of media height
        width:
          type: number
          minimum: 0.1
          maximum: 100
          description: Width as percentage of media width
        height:
          type: number
          minimum: 0.1
          maximum: 100
          description: Height as percentage of media height
        scaleX:
          type: number
          minimum: 0.01
          maximum: 10
          default: 1
          description: Horizontal scaling factor
        scaleY:
          type: number
          minimum: 0.01
          maximum: 10
          default: 1
          description: Vertical scaling factor
        angle:
          type: number
          minimum: -360
          maximum: 360
          default: 0
          description: Rotation angle in degrees
        skewX:
          type: number
          minimum: -89
          maximum: 89
          default: 0
          description: X-axis skewing in degrees
        skewY:
          type: number
          minimum: -89
          maximum: 89
          default: 0
          description: Y-axis skewing in degrees
        flipX:
          type: boolean
          default: false
          description: Horizontal flip
        flipY:
          type: boolean
          default: false
          description: Vertical flip
        originX:
          type: string
          enum:
            - left
            - center
            - right
          default: left
          description: Transform origin X
        originY:
          type: string
          enum:
            - top
            - center
            - bottom
          default: top
          description: Transform origin Y
        opacity:
          type: number
          minimum: 0
          maximum: 1
          default: 1
          description: Object opacity
        visible:
          type: boolean
          default: true
          description: Object visibility
        shadow:
          type: object
          properties:
            color:
              type: string
              default: rgba(0,0,0,0.3)
              description: Shadow color
            blur:
              type: number
              minimum: 0
              maximum: 50
              default: 5
              description: Shadow blur radius in pixels
            offsetX:
              type: number
              minimum: -10
              maximum: 10
              default: 0.2
              description: Shadow X offset as percentage of media width
            offsetY:
              type: number
              minimum: -10
              maximum: 10
              default: 0.2
              description: Shadow Y offset as percentage of media height
        strokeLineCap:
          type: string
          enum:
            - butt
            - round
            - square
          default: butt
          description: Line cap style
        strokeLineJoin:
          type: string
          enum:
            - miter
            - round
            - bevel
          default: miter
          description: Line join style
        strokeMiterLimit:
          type: number
          minimum: 1
          maximum: 20
          default: 4
          description: Miter limit for line joins
        strokeDashArray:
          type: array
          items:
            type: number
            minimum: 0.1
            maximum: 10
          maxItems: 10
          description: Dash pattern as multiples of stroke width
        fillRule:
          type: string
          enum:
            - nonzero
            - evenodd
          default: nonzero
          description: Fill rule for complex shapes
        nestedAnnotations:
          type: array
          maxItems: 10
          description: >-
            Optional array of nested annotations (spatial annotations within this annotation's context). IMPORTANT -
            Nested annotations can ONLY be used when the parent annotation has a timestamp (timestamp, startTimestamp,
            or endTimestamp). Nested annotations inherit the parent's timestamp and cannot have their own timestamps or
            nested annotations.
          items:
            $ref: '#/components/schemas/NestedAnnotation'
    PercentageCoordinates:
      type: object
      properties:
        x:
          type: number
          minimum: 0
          maximum: 100
          description: X coordinate as percentage (0-100)
        'y':
          type: number
          minimum: 0
          maximum: 100
          description: Y coordinate as percentage (0-100)
      required:
        - x
        - 'y'
    NestedAnnotation:
      oneOf:
        - type: object
          description: Nested dot annotation
          allOf:
            - $ref: '#/components/schemas/BaseAnnotation'
            - type: object
              properties:
                type:
                  type: string
                  enum:
                    - dot
                coordinates:
                  $ref: '#/components/schemas/PercentageCoordinates'
                radius:
                  type: number
                  minimum: 5
                  maximum: 100
                  default: 10
              required:
                - type
                - coordinates
        - type: object
          description: Nested shape annotation
          allOf:
            - $ref: '#/components/schemas/BaseAnnotation'
            - type: object
              properties:
                type:
                  type: string
                  enum:
                    - rectangle
                    - circle
                    - triangle
                    - arrow
                    - line
                coordinates:
                  type: array
                  items:
                    $ref: '#/components/schemas/PercentageCoordinates'
                  minItems: 2
                  maxItems: 50
                strokeColor:
                  type: string
                  default: '#000000'
                fillColor:
                  type: string
                strokeWidth:
                  type: number
                  minimum: 0
                  maximum: 50
                  default: 2
              required:
                - type
                - coordinates
        - type: object
          description: Nested text annotation
          allOf:
            - $ref: '#/components/schemas/BaseAnnotation'
            - type: object
              properties:
                type:
                  type: string
                  enum:
                    - text
                coordinates:
                  $ref: '#/components/schemas/PercentageCoordinates'
                content:
                  type: string
                  maxLength: 500
                fontSize:
                  type: number
                  minimum: 8
                  maximum: 72
                  default: 16
                fontFamily:
                  type: string
                  enum:
                    - Arial
                    - Helvetica
                    - Times New Roman
                    - Courier New
                    - Georgia
                    - Verdana
                  default: Arial
                fontWeight:
                  type: string
                  enum:
                    - normal
                    - bold
                  default: normal
                fontStyle:
                  type: string
                  enum:
                    - normal
                    - italic
                  default: normal
                textColor:
                  type: string
                  default: '#000000'
              required:
                - type
                - coordinates
                - content
        - type: object
          description: Nested path annotation
          allOf:
            - $ref: '#/components/schemas/BaseAnnotation'
            - type: object
              properties:
                type:
                  type: string
                  enum:
                    - path
                pathData:
                  type: string
                  maxLength: 10000
                strokeColor:
                  type: string
                  default: '#000000'
                strokeWidth:
                  type: number
                  minimum: 0
                  maximum: 50
                  default: 2
              required:
                - type
                - pathData
    DotAnnotation:
      allOf:
        - $ref: '#/components/schemas/BaseAnnotation'
        - type: object
          properties:
            type:
              type: string
              enum:
                - dot
            coordinates:
              $ref: '#/components/schemas/PercentageCoordinates'
            radius:
              type: number
              default: 10
              minimum: 5
              maximum: 100
          required:
            - type
            - coordinates
    FrameCommentAnnotation:
      allOf:
        - $ref: '#/components/schemas/BaseAnnotation'
        - type: object
          properties:
            type:
              type: string
              enum:
                - frameComment
          required:
            - type
    DotValidation:
      $ref: '#/components/schemas/DotAnnotation'
    FrameCommentValidation:
      $ref: '#/components/schemas/FrameCommentAnnotation'
    AssetReferences:
      type: object
      description: >
        Every location one asset is referenced. The PRIMARY file system is the asset's home; SECONDARY references are
        the reviewer tree, submissions and public releases. Renaming the asset fans out to all of them, and deleting the
        last primary reference removes all of them.
      properties:
        assetId:
          type: string
          format: uuid
        assetName:
          type: string
        primaryRoot:
          type: string
          description: Root path of the asset's home file system.
        primary:
          type: array
          description: Where the asset's own file system holds it.
          items:
            type: object
            properties:
              fileSystemId:
                type: string
                format: uuid
              path:
                type: string
              itemPath:
                type: string
              createdAt:
                type: string
                format: date-time
        secondary:
          type: object
          description: >
            References outside the home file system, keyed by collection. All four keys are always present; a collection
            with no references is an empty array.
          properties:
            reviewer:
              type: array
              items:
                $ref: '#/components/schemas/AssetReference'
            submission:
              type: array
              items:
                $ref: '#/components/schemas/AssetReference'
            public:
              type: array
              items:
                $ref: '#/components/schemas/AssetReference'
            other:
              type: array
              items:
                $ref: '#/components/schemas/AssetReference'
        counts:
          type: object
          description: Count per collection. Only collections with at least one reference appear.
        secondaryCount:
          type: integer
          description: Total secondary references. Zero means the asset lives only in its own folder.
      required:
        - assetId
        - assetName
        - primaryRoot
        - primary
        - secondary
        - counts
        - secondaryCount
    AssetReference:
      type: object
      description: One location outside the asset's home file system.
      properties:
        fileSystemId:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - reviewer
            - submission
            - public
            - other
        path:
          type: string
        itemPath:
          type: string
        label:
          type: string
          description: Human-readable location, e.g. "Submission / Round 2".
        createdAt:
          type: string
          format: date-time
    Tasks:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/Task'
    Task:
      type: object
      description: >-
        A task created from a chat message mention or on a project board. Timestamps are ISO 8601 strings. The
        `creator`, `project`, `origin`, `relatedTo`, `assignedTo` and `inheritance` keys are aliases of the matching
        `*Id` fields kept for backward compatibility.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: The unique identifier of the task
        creatorId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: The creators user ID.
        creator:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: Alias of creatorId.
        projectId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: The ID of the project the task belongs to.
        project:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: Alias of projectId.
        assignedToId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: The user the task is assigned to. Omitted when unassigned.
        assignedTo:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: Alias of assignedToId. Omitted when unassigned.
        originId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: The ID of the resource where the task was created. Null for tasks created directly on a board.
          nullable: true
        origin:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: Alias of originId.
          nullable: true
        originType:
          type: string
          enum:
            - chatMessage
          nullable: true
          description: The type of resource the task originated from. Null for tasks created directly on a board.
        relatedToId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: >-
            The ID of the resource the task is related to. For example of the task was related to a particular asset
            this would be the asset ID.
        relatedTo:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: Alias of relatedToId.
        relatedToType:
          type: string
          enum:
            - project
            - asset
            - chatMember
            - chatSubmission
          description: The type of resource the task is related to.
        status:
          type: string
          enum:
            - pending
            - inProgress
            - complete
            - closed
          default: pending
          description: Status of the task.
        acknowledged:
          type: boolean
          default: false
          description: Whether the mentioned user has acknowledged the task.
        visibility:
          type: string
          enum:
            - creator
            - reviewer
            - user
          nullable: true
          description: Which side of the project the task is visible to.
        mentions:
          type: array
          items:
            $ref: '#/components/schemas/UUID'
          description: IDs of users mentioned by the task.
        followers:
          type: array
          items:
            $ref: '#/components/schemas/UUID'
          description: IDs of users following the task.
        subject:
          type: string
          description: Short description of the task.
        body:
          type: string
          nullable: true
          description: Body of the task.
        description:
          type: string
          nullable: true
          description: Rich description for board tasks.
        boardId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: The board the task belongs to. Null for legacy mention tasks not on a board.
          nullable: true
        columnId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: The board column the task is in.
          nullable: true
        sortOrder:
          type: integer
          default: 0
          description: Position within the column.
        taskNumber:
          type: integer
          nullable: true
          description: Per-project auto-incrementing task number (#1,
        tags:
          type: array
          items:
            $ref: '#/components/schemas/UUID'
          description: Project-scoped tag IDs applied to the task.
        inheritanceId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: ID of the inheritance record. Omitted when not set.
        inheritance:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: Alias of inheritanceId. Omitted when not set.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - creatorId
        - projectId
        - relatedToId
        - relatedToType
        - status
        - subject
    LastSeen:
      type: object
      description: An object formated {[channel]:timestamp} showing the last time a user has viewed a channel.
      example: '{/workspace/[workspaceId]:[timestamp]}'
      properties:
        userId:
          type: string
          format: uuid
        channels:
          type: array
          items:
            type: string
        types:
          type: array
          items:
            type: string
        lastSeen:
          type: integer
          format: int64
        createdAt:
          type: integer
          format: int64
          example: 1678886400000
        updatedAt:
          type: integer
          format: int64
          example: 1678886400000
    Notification:
      type: object
      description: >-
        A platform notification. `createdAt` is an ISO 8601 string; notifications have no `updatedAt`. Internal push /
        webhook delivery fields are never returned.
      properties:
        id:
          type: string
          format: uuid
          description: The unique identifier of the notification
        _id:
          type: string
          format: uuid
          description: Deprecated alias of `id` kept for backward compatibility.
        type:
          type: string
          description: The type of notification as defined in config/notifications.
        initiatorId:
          type: string
          format: uuid
          nullable: true
          description: The ID of the user who initiated the notification. Null for system notifications.
        initiatorType:
          type: string
          description: Whether a user or the system initiated the notification.
          enum:
            - user
            - system
          default: user
        initiator:
          allOf:
            - $ref: '#/components/schemas/PublicUser'
          description: The public user information for the initiator of the notification. Not present for system notifications.
        resourceId:
          type: string
          format: uuid
          nullable: true
          description: The ID of the resource the notification is about.
        resourceType:
          type: string
          nullable: true
          description: The type of resource the notification is about.
          enum:
            - workspace
            - project
            - asset
            - chat
            - chatMember
            - chatMessage
            - chatSubmission
            - convo
            - user
            - folder
            - invite
            - membership
            - task
            - board
            - lastSeen
            - summary
            - subscription
            - tag
            - public
            - settings
        channels:
          type: array
          items:
            type: string
          description: The channels the notification was emitted to (see the Websocket documentation).
        tokens:
          type: object
          description: Tokens as defined by the notifications template in config/notifications
        changes:
          type: object
          nullable: true
          description: The changes that occurred to the resource. Not all keys may be present.
          properties:
            create:
              type: array
              items:
                type: object
                properties:
                  resourceId:
                    type: string
                    format: uuid
                  resourceType:
                    type: string
                  resource:
                    type: object
                    description: The resource that was created.
            update:
              type: array
              items:
                type: object
                properties:
                  resourceId:
                    type: string
                    format: uuid
                  resourceType:
                    type: string
                  oldResource:
                    type: object
                    description: The resource before the update. ( Optional )
                  resource:
                    type: object
                    description: The resource that was updated.
            delete:
              type: array
              items:
                type: object
                properties:
                  resourceId:
                    type: string
                    format: uuid
                  resourceType:
                    type: string
        scheduledSendAt:
          type: string
          format: date-time
          nullable: true
          description: When a delayed push for this notification is scheduled to send.
        createdAt:
          type: string
          format: date-time
          description: The timestamp when the notification was created
      required:
        - id
        - type
        - tokens
        - initiatorType
        - channels
    UUID:
      type: string
      format: uuid
      pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
    AssetAndSignedLinks:
      type: array
      items:
        $ref: '#/components/schemas/AssetAndSignedLink'
    AssetAndSignedLink:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 1
        name:
          type: string
          example: file1.png
        hash:
          type: string
          example: d9729feb74992cc3482b350163a1a010
        type:
          type: string
          example: image
        asset:
          $ref: '#/components/schemas/Asset'
        signedUrlData:
          $ref: '#/components/schemas/SignedUrlData'
        status:
          type: string
          example: success
    RepairAssetAndSignedLinks:
      type: array
      items:
        $ref: '#/components/schemas/RepairAssetAndSignedLink'
    RepairAssetAndSignedLink:
      type: object
      properties:
        assetId:
          $ref: '#/components/schemas/UUID'
        status:
          type: string
          example: success
        asset:
          $ref: '#/components/schemas/Asset'
        uploadChunkSizeInBytes:
          type: integer
          format: int64
          example: 209715200
        signedUrlData:
          $ref: '#/components/schemas/SignedUrlData'
    SignedUrlData:
      type: object
      properties:
        fileName:
          type: string
          example: file1.png
        ownerAssetId:
          type: string
          example: 65af4c7b22ac19c0b1648241
        key:
          type: string
          example: /workspaces/ws-65af4c7b22ac19c0b1648226/projects/pr...
        urls:
          type: array
          items:
            format: uri
            example: https://s3.eu-west-1.amazonaws.com/files-test.nurama.io...
        mimeType:
          type: string
          example: image/png
        expires:
          type: integer
          format: int64
          example: 1705988395
        status:
          type: string
          example: success
    DownloadSignedUrlData:
      type: object
      properties:
        fileName:
          type: string
          example: file1.png
        ownerAssetId:
          type: string
          example: 65af4c7b22ac19c0b1648241
        key:
          type: string
          example: /workspaces/ws-65af4c7b22ac19c0b1648226/projects/pr...
        url:
          type: string
          example: https://s3.eu-west-1.amazonaws.com/files-test.nurama.io...
        mimeType:
          type: string
          example: image/png
        expires:
          type: integer
          format: int64
          example: 1705988395
        status:
          type: string
          example: success
    Memberships:
      type: array
      items:
        $ref: '#/components/schemas/Membership'
    Membership:
      type: object
      description: A user's membership of a workspace or project. Timestamps are ISO 8601 strings.
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        resourceId:
          type: string
          format: uuid
        resourceType:
          type: string
          enum:
            - workspace
            - project
          example: workspace
        roles:
          type: array
          items:
            type: string
            enum:
              - workspaceOwner
              - workspaceAdmin
              - workspaceMember
              - workspaceChatMember
              - projectOwner
              - projectAdmin
              - creator
              - reviewer
              - reviewerAdmin
              - reviewerBoardManager
          example:
            - workspaceOwner
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        resource:
          description: Populated resource, when the request populates it.
          oneOf:
            - $ref: '#/components/schemas/Workspace'
            - $ref: '#/components/schemas/Project'
        user:
          allOf:
            - $ref: '#/components/schemas/PublicUser'
          description: Populated public user, when the request populates it.
      required:
        - id
        - userId
        - resourceId
        - resourceType
        - roles
    MembershipReport:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/PublicUser'
                  resources:
                    type: array
                    items:
                      type: object
                      properties:
                        resourceId:
                          type: string
                          example: 6733736868949390a715fc63
                        resourceType:
                          type: string
                          example: project
                        name:
                          type: string
                          example: Orn Group
                        roles:
                          type: array
                          items:
                            type: string
                            example: reviewer, creator, projectOwner ect.
                  membershipRecords:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          example: 6733736868949390a715fc63
                        resourceId:
                          type: string
                          example: 6733736868949390a715fc63
                        resourceType:
                          type: string
                          example: project
                        roles:
                          items:
                            type: string
                            example: creator
                  createdAt:
                    type: string
                    format: date-time
                    example: '2024-11-12T15:25:28.620Z'
                  updatedAt:
                    type: string
                    format: date-time
                    example: '2024-11-12T15:25:28.620Z'
                  userId:
                    type: string
                    example: 6733736768949390a715fac4
                  roles:
                    type: array
                    items:
                      type: string
                      example: reviewer
                  isBillable:
                    type: boolean
                    example: false
    Mentionable:
      type: array
      items:
        allOf:
          - $ref: '#/components/schemas/PublicUser'
          - properties:
              roles:
                type: array
                items:
                  type: string
                  example: reviewer
    Invites:
      type: array
      items:
        $ref: '#/components/schemas/Invite'
    Invite:
      type: object
      description: >-
        An invitation to join a workspace or project. `createdAt` and `updatedAt` are Unix millisecond timestamps;
        `expires` is an ISO 8601 string. The invite's `id` doubles as the invite token used in accept links.
      properties:
        id:
          type: string
          format: uuid
          example: 0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b
        inviterId:
          type: string
          format: uuid
        inviteeEmail:
          type: string
          format: email
          example: bernice_koss@hotmail.com
        inviteeId:
          type: string
          format: uuid
          nullable: true
          description: Only set once the invitee has an account on the platform.
        resourceId:
          type: string
          format: uuid
        resourceType:
          type: string
          enum:
            - workspace
            - project
          example: workspace
        role:
          type: string
          enum:
            - workspaceAdmin
            - projectAdmin
            - creator
            - reviewer
            - reviewerAdmin
          example: workspaceAdmin
        additionalRoles:
          type: array
          items:
            type: string
            enum:
              - workspaceChatMember
              - workspaceAdmin
          description: Additive roles granted alongside `role` when the invite is accepted.
        expires:
          type: string
          format: date-time
          example: '2023-12-15T12:12:34.784Z'
        status:
          type: string
          enum:
            - active
            - canceled
            - accepted
          example: active
        createdAt:
          type: integer
          format: int64
          example: 1704461497862
        updatedAt:
          type: integer
          format: int64
          example: 1704461497862
        resource:
          description: The invited-to resource when populated.
          oneOf:
            - $ref: '#/components/schemas/Workspace'
            - $ref: '#/components/schemas/Project'
        inviter:
          description: The inviter's user ID, or the populated public user when the request populates it.
          oneOf:
            - type: string
              format: uuid
            - $ref: '#/components/schemas/PublicUser'
        invitee:
          nullable: true
          description: >-
            The invitee's user ID (null until they have an account), or the populated public user when the request
            populates it.
          oneOf:
            - type: string
              format: uuid
            - $ref: '#/components/schemas/PublicUser'
      required:
        - id
        - inviterId
        - inviteeEmail
        - resourceId
        - resourceType
        - role
        - expires
        - status
    Chat:
      type: object
      description: A chat thread. Timestamps are ISO 8601 strings.
      properties:
        id:
          type: string
          format: uuid
        chatType:
          type: string
          enum:
            - topic
            - member
            - submission
            - ai
            - support
          default: topic
          description: >-
            Topic chats hang off a project/asset/public/task, member chats are direct chats between members, submission
            chats belong to a review submission, ai and support chats are conversations with the assistant / support.
        topicId:
          type: string
          format: uuid
          description: The ID of the topic the chat is attached to.
        topicType:
          type: string
          enum:
            - asset
            - project
            - public
            - task
          description: The type of resource the chat is about.
        replyType:
          type: string
          default: quote
          description: How replies are rendered in this chat.
        subject:
          type: string
          nullable: true
          description: The subject of the chat, such as an asset's file name.
        visibility:
          type: string
          enum:
            - creator
            - reviewer
            - member
            - public
          description: Controls which type of members can access the chat.
        publicId:
          type: string
          format: uuid
          nullable: true
          description: Links to the Public release when visibility is `public` and topicType is `asset`.
        participants:
          type: array
          items:
            type: string
            format: uuid
          description: Users who have participated in the chat.
        following:
          type: array
          items:
            type: string
            format: uuid
          description: Users following the chat.
        annotations:
          type: array
          items:
            type: object
          description: Annotations attached to the chat.
        totalMessages:
          type: integer
          description: Total number of messages in chat.
        lastMessageAt:
          type: string
          format: date-time
          nullable: true
        recentMessages:
          type: array
          items:
            oneOf:
              - type: string
                format: uuid
              - $ref: '#/components/schemas/ChatMessage'
          description: Array of IDs of messages of the most recent chats or the actual chats depending on request options.
        activeConvoId:
          type: string
          format: uuid
          nullable: true
          description: The active convo (call) on this chat, if any.
        activeConvo:
          nullable: true
          description: Alias of activeConvoId, or the populated convo when the request populates it.
          oneOf:
            - type: string
              format: uuid
            - type: object
        status:
          type: string
          enum:
            - active
            - inactive
            - pendingDelete
          description: The status of the chat.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - chatType
        - topicId
        - topicType
        - visibility
    ChatMember:
      type: object
      properties:
        scopeId:
          type: string
          format: uuid
          description: The ID of the scope the chat is about.
        scopeType:
          type: string
          enum:
            - project
            - workspace
            - social
          description: Scope of chat. Determines who can be invited to the chat.
        scope:
          type: object
          description: Populated scope data (project or workspace) based on scopeType. Includes logo asset when populated.
          properties:
            id:
              type: string
              format: uuid
              description: The ID of the scope (project or workspace).
            name:
              type: string
              description: The name of the scope.
            slug:
              type: string
              description: The slug of the scope.
            logo:
              oneOf:
                - type: string
                  format: uuid
                - $ref: '#/components/schemas/Asset'
              description: The logo asset of the scope.
        subject:
          type: string
          description: The subject of the chat, such as an asset's file name.
        icon:
          type: string
          format: uuid
          description: The ID of the asset used as the chat icon.
          nullable: true
        members:
          type: array
          items:
            type: string
            format: uuid
          description: >-
            Only for private/memeber chats. Users who can currently access the chat. For project/asset chats access is
            determined by visibility and the topic.
        participants:
          type: array
          items:
            type: string
            format: uuid
          description: Users who have participated in the chat.
        totalMessages:
          type: number
          description: Total number of messages in chat.
        recentMessages:
          type: array
          items:
            oneOf:
              - type: string
                format: uuid
              - $ref: '#/components/schemas/ChatMessage'
            description: Array of IDs of messages of the most recent chats or the actual chats depending on request options.
        status:
          type: string
          enum:
            - active
            - inactive
            - pendingDelete
          description: The status of the chat.
        createdBefore:
          type: integer
          description: Timestamp of query. Used to generate future paginated requests.
        createdAt:
          type: integer
          format: int64
          description: The timestamp when the chat member object was created.
          example: 1678886400000
        updatedAt:
          type: integer
          format: int64
          description: The timestamp when the chat member object was last updated.
          example: 1678886400000
      required:
        - scopeId
        - scopeType
    ChatSubmission:
      type: object
      properties:
        chatType:
          type: string
          enum:
            - submission
          default: submission
        projectId:
          type: string
          format: uuid
          description: The ID of the project the chat is about
        project:
          items:
            oneOf:
              - type: string
                format: uuid
              - $ref: '#/components/schemas/Project'
          description: Project ID or populated project data.
        publisherId:
          type: string
          format: uuid
          description: The ID of the user who published the submission.
        publisher:
          items:
            oneOf:
              - type: string
                format: uuid
              - $ref: '#/components/schemas/PublicUser'
          description: User ID or populated public user data.
        subject:
          type: string
          description: The subject of the chat, such as an asset's file name.
        description:
          type: string
          description: Description of the submission.
        version:
          type: string
          description: Version of the submission.
        assetIds:
          type: array
          items:
            oneOf:
              - type: string
                format: uuid
              - $ref: '#/components/schemas/ChatMessage'
          description: Array of asset IDs or populatd assets associated with the submission.
        assetInventory:
          type: object
          description: Inventory of asset media type and their count. Example {image:6,video:7}
        participants:
          type: array
          items:
            type: string
            format: uuid
          description: Users who have participated in the chat.
        annotations:
          type: array
          items:
            type: object
          description: Array of annotations.
        totalMessages:
          type: number
          description: Total number of messages in chat.
        lastMessageAt:
          type: string
          format: date-time
          description: Timestamp of the last message.
        recentMessages:
          type: array
          items:
            oneOf:
              - type: string
                format: uuid
              - $ref: '#/components/schemas/ChatMessage'
            description: Array of IDs of messages of the most recent chats or the actual chats depending on request options.
        tags:
          type: array
          items:
            type: string
            format: uuid
          description: Tags associated with the submission
          default: []
        status:
          type: string
          enum:
            - active
            - inactive
            - pendingDelete
          description: The status of the chat
        createdAt:
          type: integer
          format: int64
          description: Timestamp of creation
          example: 1678886400000
        updatedAt:
          type: integer
          format: int64
          description: Timestamp of last update
          example: 1678886400000
      required:
        - chatType
        - projectId
        - publisherId
        - status
    ChatMessage:
      type: object
      description: A message in a chat. `createdAt` and `updatedAt` are Unix millisecond timestamps.
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum:
            - user
            - system
          description: Whether the message comes from a user or the system.
        dataType:
          type: string
          nullable: true
          enum:
            - notification
            - chatMessage
            - convo
          description: Type of resource `data` points to for system messages.
        dataId:
          type: string
          format: uuid
          nullable: true
          description: ID of the resource of `dataType`.
        data:
          description: Alias of dataId. For system messages this may be populated with the referenced object of `dataType`.
          oneOf:
            - type: string
              format: uuid
            - type: object
        chatId:
          type: string
          format: uuid
          description: The ID of the chat the message belongs to.
        chatType:
          type: string
          enum:
            - topic
            - member
            - submission
            - ai
            - support
          description: Type of the chat the message belongs to.
        authorId:
          type: string
          format: uuid
          nullable: true
          description: >-
            The ID of the user who authored the message. Null for anonymous guest messages posted on a public release
            (see guest fields below).
        author:
          description: >-
            Populated author (a public user, or a synthesized guest author with isGuest true) when the request populates
            authors.
          oneOf:
            - $ref: '#/components/schemas/PublicUser'
            - type: object
              properties:
                id:
                  type: string
                  format: uuid
                  nullable: true
                displayName:
                  type: string
                color:
                  type: string
                  nullable: true
                isGuest:
                  type: boolean
                  enum:
                    - true
        guestId:
          type: string
          format: uuid
          nullable: true
          description: >-
            Client-generated identity of an anonymous guest commenter (localStorage UUID). Set only when authorId is
            null.
        guestName:
          type: string
          nullable: true
          description: Display name of an anonymous guest commenter. Set only when authorId is null.
        guestColor:
          type: string
          nullable: true
          description: Approved color an anonymous guest commenter selected. Set only when authorId is null.
        replyToId:
          type: string
          format: uuid
          nullable: true
          description: If this message is a reply to another this is the ID of the message it is a reply to.
          default: null
        content:
          type: string
          nullable: true
          description: The content of the chat message.
          maxLength: 10000
        mentions:
          type: array
          items:
            oneOf:
              - type: string
                format: uuid
              - $ref: '#/components/schemas/PublicUser'
          description: IDs of users mentioned in the message, or the populated public users depending on request options.
        assetMentions:
          type: array
          items:
            type: string
            format: uuid
          description: IDs of assets mentioned in the message.
        folderMentions:
          type: array
          items:
            type: string
            format: uuid
          description: IDs of folders mentioned in the message.
        submissionMentions:
          type: array
          items:
            type: string
            format: uuid
          description: IDs of submissions mentioned in the message.
        publicMentions:
          type: array
          items:
            type: string
          description: Tokens of public releases mentioned in the message (short URL-safe strings, not UUIDs).
        taskMentions:
          type: array
          items:
            type: string
            format: uuid
          description: IDs of board tasks mentioned as chips in the message.
        taskCards:
          type: array
          items:
            type: string
            format: uuid
          description: IDs of tasks rendered as full inline cards in the message.
        suggestedTasks:
          type: array
          description: Tasks suggested by the AI assistant for this message.
          items:
            type: object
            properties:
              subject:
                type: string
              description:
                type: string
              assignedToId:
                type: string
                format: uuid
              boardId:
                type: string
                format: uuid
              columnId:
                type: string
                format: uuid
        quotes:
          type: array
          items:
            type: string
            format: uuid
          description: IDs of messages quoted by this message.
        attachments:
          type: array
          items:
            oneOf:
              - type: string
                format: uuid
              - $ref: '#/components/schemas/Asset'
          description: Array of IDs of Assets attached to the chat or the actual Asset objects.
        revisions:
          type: array
          items:
            type: object
          description: When a message is edited a copy of the previous content is stored here as a revision.
        meta:
          type: object
          nullable: true
          description: Additional metadata for the chat message.
        status:
          type: string
          enum:
            - active
            - pendingDelete
            - userDeleted
          description: The status of the chat message.
        totalReplies:
          type: integer
          description: Total number of replies attached to message.
        recentReplies:
          type: array
          items:
            oneOf:
              - type: string
                format: uuid
              - $ref: '#/components/schemas/ChatMessage'
          description: Array of IDs of Chat-Messages or the actual chats Chat-Message depending on request options.
        annotations:
          type: array
          description: Annotations attached to the message (dots, frame comments, drawings, ...). See the Annotation schemas.
          items:
            oneOf:
              - $ref: '#/components/schemas/DotValidation'
              - $ref: '#/components/schemas/FrameCommentValidation'
              - type: object
        linkPreviews:
          type: array
          items:
            $ref: '#/components/schemas/LinkPreview'
          description: Array of link previews extracted from the message content.
        reactions:
          type: array
          items:
            $ref: '#/components/schemas/Reaction'
          description: Array of reactions on this message from different users.
        highlighted:
          type: boolean
          default: false
        highlightedById:
          type: string
          format: uuid
          nullable: true
          description: The user who highlighted the message.
        isConvoMessage:
          type: boolean
          default: false
          description: True for system messages generated by a convo (call) lifecycle event.
        scopeId:
          type: string
          format: uuid
          nullable: true
          description: The project (or other scope) the message belongs to.
        scopeType:
          type: string
          nullable: true
          default: project
        scopeVisibility:
          type: array
          items:
            type: string
            enum:
              - creator
              - reviewer
        inheritanceId:
          type: string
          format: uuid
          nullable: true
        aiRole:
          type: string
          nullable: true
          enum:
            - assistant
            - tool
          description: Set on messages authored by the AI assistant. Null on plain user messages.
        aiModel:
          type: string
          nullable: true
          description: Model identifier used to generate an assistant message.
        aiTokensIn:
          type: integer
          nullable: true
        aiTokensOut:
          type: integer
          nullable: true
        aiToolCalls:
          type: array
          nullable: true
          items:
            type: object
        aiToolResults:
          type: array
          nullable: true
          items:
            type: object
        aiUsageEventId:
          type: string
          format: uuid
          nullable: true
        createdAt:
          type: integer
          format: int64
          description: Timestamp of creation
          example: 1678886400000
        updatedAt:
          type: integer
          format: int64
          description: Timestamp of last update
          example: 1678886400000
      required:
        - id
        - type
        - chatId
        - chatType
    Reaction:
      type: object
      properties:
        user:
          type: string
          format: uuid
          description: The ID of the user who created the reaction.
        emoji:
          type: string
          description: The emoji character(s) used for the reaction.
          minLength: 1
          maxLength: 10
          example: 👍
      required:
        - user
        - emoji
    LinkPreview:
      type: object
      description: Rich link preview metadata extracted from a URL (Open Graph / meta tags).
      properties:
        url:
          type: string
          format: uri
          description: The original URL.
        title:
          type: string
          nullable: true
          description: Page title from og:title or <title> tag.
        description:
          type: string
          nullable: true
          description: Page description from og:description or meta description.
        image:
          type: string
          format: uri
          nullable: true
          description: Image URL from og:image.
        siteName:
          type: string
          nullable: true
          description: Site name from og:site_name.
        favicon:
          type: string
          format: uri
          nullable: true
          description: Favicon URL.
        signature:
          type: string
          description: HMAC-SHA256 signature for anti-spoofing verification (transient, not persisted).
          pattern: ^[a-f0-9]{64}$
      required:
        - url
    Folder:
      type: object
      description: A folder in a project, submission or public file system. Timestamps are ISO 8601 strings.
      properties:
        id:
          type: string
          format: uuid
          description: The unique identifier of the folder.
        ownerResourceId:
          type: string
          format: uuid
          description: The ID of the resource the folder belongs to.
        ownerResourceType:
          type: string
          enum:
            - project
            - submission
            - public
          description: The type of resource the folder belongs to.
        creatorId:
          type: string
          format: uuid
          description: The ID of the user who created the folder.
        creator:
          allOf:
            - $ref: '#/components/schemas/PublicUser'
          description: Populated creator, when the request populates it.
        publisherId:
          type: string
          format: uuid
          nullable: true
          description: The user who published the folder, when published.
        inheritanceId:
          type: string
          format: uuid
          nullable: true
        iconId:
          type: string
          format: uuid
          nullable: true
          description: The ID of the asset used as the folder icon.
        icon:
          description: >-
            Alias of iconId, or the populated icon asset when the request populates it. Omitted when the folder has no
            icon.
          oneOf:
            - type: string
              format: uuid
            - $ref: '#/components/schemas/Asset'
        color:
          type: string
          description: Hex color code for the folder.
          nullable: true
        name:
          type: string
          description: The name of the folder.
        slug:
          type: string
          nullable: true
          description: URL-friendly version of the folder name.
        fileSystemPaths:
          type: array
          items:
            type: string
          description: File system paths the folder appears at.
        basePath:
          type: string
          nullable: true
          description: Base path of the folder within its file system.
        visibility:
          type: string
          nullable: true
          enum:
            - creator
            - reviewer
            - member
            - public
        noFileSystem:
          type: boolean
          default: false
          description: True when the folder is not represented in a file system tree.
        tags:
          type: array
          items:
            type: string
            format: uuid
          description: Tags associated with the folder.
          default: []
        status:
          type: string
          enum:
            - active
            - pendingDelete
          description: The status of the folder.
          default: active
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - ownerResourceId
        - ownerResourceType
        - creatorId
        - name
    File:
      type: object
      properties:
        assetId:
          type: string
          format: uuid
          description: ID of the asset the file belongs to.
        keyPath:
          type: string
          description: The path the file is available at when added to the base media url.
          example: /project/asset/file123.jpg
        checksum:
          type: string
          description: The checksum of the file.
        checksumAlgorithm:
          type: string
          enum:
            - md5
          description: This is a placeholder for sometime in the future where we may require multiple hashing algorithms.
          default: md5
        name:
          type: string
          description: The full name of the file, including extension.
        functionType:
          type: string
          enum:
            - original
            - thumbnail
            - stream
            - media
            - customThumbnailOriginal
            - customThumbnail
            - documentText
          description: This type is used to determine the file's purpose, not the file type (video, image, etc.).
        sizeInBytes:
          type: number
          description: The size of the file in bytes.
        status:
          type: string
          enum:
            - active
            - pendingDelete
            - inactive
          description: The status of the file.
          default: active
        meta:
          type: object
          description: Additional metadata for the file.
          default: {}
        createdAt:
          type: integer
          format: int64
          description: The timestamp when the file was created.
          example: 1678886400000
        updatedAt:
          type: integer
          format: int64
          description: The timestamp when the file was last updated.
          example: 1678886400000
      required:
        - assetId
        - keyPath
        - name
        - sizeInBytes
        - status
    Asset:
      type: object
      description: >-
        An uploaded asset (image, video, audio, document, 3D model or generic file). `createdAt`, `updatedAt` and
        `publishedOn` are Unix millisecond timestamps.
      properties:
        id:
          type: string
          format: uuid
          description: The unique identifier of the asset
        ownerResourceId:
          type: string
          format: uuid
          description: The id of the resource that the asset is attached to.
        ownerResourceType:
          type: string
          enum:
            - workspace
            - project
            - user
            - chat
            - chatMessage
          description: The type of the owner resource.
        creatorId:
          type: string
          format: uuid
          description: The ID of the user who created the asset.
        creator:
          allOf:
            - $ref: '#/components/schemas/PublicUser'
          description: Populated creator, when the request populates it.
        fileSystemPaths:
          type: array
          items:
            type: string
          description: File system paths the asset appears at.
        basePath:
          type: string
          nullable: true
          description: Base path of the asset within its file system.
        noFileSystem:
          type: boolean
          default: false
          description: True when the asset is not represented in a file system tree (avatars, logos, chat attachments).
        keyPath:
          type: string
          description: The storage key prefix used for the asset's files. Needed by clients to construct CDN URLs.
        checksum:
          type: string
          description: Checksum of the original file.
        checksumAlgorithm:
          type: string
          nullable: true
          example: md5
          description: The algorithm used for computing the checksum.
        name:
          type: string
          description: The name of the asset used on the platform.
        slug:
          type: string
          nullable: true
          description: URL-safe slug generated from the asset name with UUID.
        functionType:
          type: string
          enum:
            - attachment
            - media
            - avatar
            - logo
            - icon
          description: The function of the asset on the platform.
        mediaType:
          type: string
          enum:
            - image
            - video
            - audio
            - document
            - 3d
            - file
          description: The media type of the asset.
        tags:
          type: array
          items:
            type: string
            format: uuid
          description: Tags associated with the asset.
          default: []
        files:
          type: array
          items:
            $ref: '#/components/schemas/File'
          description: >-
            The files associated with the asset. Internal fields (bucketName, internalUrl, postProcessingTasks) are
            stripped.
        chats:
          type: object
          description: Chat information for the asset
          properties:
            creatorLastMessageAt:
              type: string
              format: date-time
              description: Timestamp of last creator chat message
            reviewerLastMessageAt:
              type: string
              format: date-time
              description: Timestamp of last reviewer chat message
            creator:
              type: string
              format: uuid
              description: Creator chat ID
            reviewer:
              type: string
              format: uuid
              description: Reviewer chat ID
        expectedUploadSizeInMB:
          type: number
          description: Expected upload size in megabytes.
          default: 0
        sizeInBytes:
          type: number
          description: The total size of the asset in bytes.
          default: 0
        visibility:
          type: array
          items:
            type: string
            enum:
              - creator
              - reviewer
              - member
              - public
          description: The visibility settings for the asset.
          default:
            - creator
        publishedOn:
          type: integer
          format: int64
          nullable: true
          description: Unix millisecond timestamp when the asset was published. Null if not published.
          example: 1678886400000
        publisherId:
          type: string
          format: uuid
          nullable: true
          description: ID of the user who published the asset
        publisher:
          allOf:
            - $ref: '#/components/schemas/PublicUser'
          description: Populated publisher, when the request populates it.
        hasActivePublicLink:
          type: boolean
          default: false
          description: True while at least one public asset link for the asset is active.
        hasActivePublicFileSystem:
          type: boolean
          default: false
          description: True while the asset is part of at least one active public release.
        everPublic:
          type: boolean
          default: false
          description: True once the asset has ever been shared publicly.
        publicFileSystemIds:
          type: array
          items:
            type: string
            format: uuid
          description: Public releases the asset belongs to.
        publicAssetLinkIds:
          type: array
          items:
            type: string
            format: uuid
          description: Public asset links created for the asset.
        submissionFileSystemIds:
          type: array
          items:
            type: string
            format: uuid
          description: Submissions the asset belongs to.
        aiGenerated:
          type: boolean
          default: false
          description: True when the asset's bytes were produced by a platform AI process (e.g. an AI revision).
        promotedFromAssetId:
          type: string
          format: uuid
          nullable: true
          description: When the asset was created by promoting a chat attachment into a project, the source attachment asset's ID.
        contentSnippet:
          type: string
          nullable: true
          description: Leading excerpt of a document's extracted text.
        status:
          type: string
          enum:
            - active
            - pendingUpload
            - pendingPostProcessing
            - postProcessingError
            - pendingDelete
            - inactive
          description: The status of the asset.
          default: active
        meta:
          type: object
          nullable: true
          description: Additional metadata for the asset (dimensions, duration, document info, ...).
        inheritanceId:
          type: string
          format: uuid
          nullable: true
          description: ID of the inheritance record for this asset
        inheritance:
          allOf:
            - $ref: '#/components/schemas/Inheritance'
          description: Populated inheritance record, when the request populates it.
        createdAt:
          type: integer
          format: int64
          description: Unix millisecond timestamp when the asset was created.
          example: 1678886400000
        updatedAt:
          type: integer
          format: int64
          description: Unix millisecond timestamp when the asset was last updated.
          example: 1678886400000
      required:
        - id
        - ownerResourceId
        - ownerResourceType
        - creatorId
        - name
        - functionType
        - mediaType
        - status
    Inheritance:
      type: object
      description: >-
        Permission inheritance chain for a resource. `inheritance` maps each ancestor resource ID (and the resource
        itself) to its resource type; `inheritanceInverted` maps resource type to resource ID.
      properties:
        id:
          type: string
          format: uuid
        resourceId:
          type: string
          format: uuid
        resourceType:
          type: string
        inheritance:
          type: object
          additionalProperties:
            type: string
          example:
            0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b: workspace
            0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c: project
        inheritanceInverted:
          type: object
          additionalProperties:
            type: string
            format: uuid
        path:
          type: string
          description: Slash separated path of ancestor IDs.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Projects:
      type: array
      items:
        $ref: '#/components/schemas/Project'
    Project:
      type: object
      description: A project within a workspace. `createdAt` and `updatedAt` are Unix millisecond timestamps.
      properties:
        id:
          type: string
          format: uuid
          description: The unique identifier of the project
        workspaceId:
          type: string
          format: uuid
          description: Id of workspace project belongs to.
        name:
          type: string
          maxLength: 100
          description: The name of the project
        slug:
          type: string
          description: URL-friendly version of the project name
        color:
          type: string
          description: Hex color code for the project. Omitted when not set.
        logo:
          description: >-
            The project logo asset ID, or the populated logo Asset when the request populates it. Omitted when the
            project has no logo.
          oneOf:
            - type: string
              format: uuid
            - $ref: '#/components/schemas/Asset'
        settings:
          type: object
          description: >-
            Per-project setting overrides (currently AI feature flags). Only present when the project overrides at least
            one key; absent keys inherit from the workspace settings.
          additionalProperties: true
        status:
          type: string
          enum:
            - active
            - inactive
            - pendingDelete
          description: The status of the project
          default: active
        createdAt:
          type: integer
          format: int64
          description: The timestamp when the project was created.
          example: 1678886400000
        updatedAt:
          type: integer
          format: int64
          description: The timestamp when the project was last updated.
          example: 1678886400000
      required:
        - id
        - workspaceId
        - name
        - status
    Workspace:
      type: object
      description: A workspace. `createdAt` and `updatedAt` are Unix millisecond timestamps.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          maxLength: 100
        slug:
          type: string
        description:
          type: string
          maxLength: 350
          nullable: true
        logo:
          description: >-
            The workspace logo asset ID, or the populated logo Asset when the request populates it. Omitted when the
            workspace has no logo.
          oneOf:
            - type: string
              format: uuid
            - $ref: '#/components/schemas/Asset'
        icon:
          description: >-
            The workspace icon asset ID, or the populated icon Asset when the request populates it. Omitted when the
            workspace has no icon.
          oneOf:
            - type: string
              format: uuid
            - $ref: '#/components/schemas/Asset'
        color:
          description: Hex color code.
          type: string
          nullable: true
        settings:
          type: object
          description: >-
            Workspace settings (AI feature flags such as aiChatEnabled, aiAssistEnabled, aiImageRevisionEnabled,
            dailyUserAiSpendBudget, ...). Only present when the workspace has changed a setting from the defaults.
          additionalProperties: true
        roles:
          type: array
          items:
            type: string
          description: >-
            The requesting user's roles on the workspace. Only present on endpoints that add it (e.g. listing a user's
            workspaces).
        status:
          type: string
          enum:
            - active
            - inactive
            - pendingDelete
        createdAt:
          type: integer
          format: int64
          description: The timestamp when the workspace was created.
          example: 1678886400000
        updatedAt:
          type: integer
          format: int64
          description: The timestamp when the workspace was last updated.
          example: 1678886400000
      required:
        - id
        - name
        - slug
        - status
    User:
      type: object
      description: >-
        The authenticated user's own profile. Timestamps are ISO 8601 strings. Auth secrets (password, MFA
        secrets/backup codes) and internal fields (linkedAuthProviders, resourceLastSeen, resourceSettings) are never
        returned.
      properties:
        id:
          type: string
          format: uuid
          description: User's Id
        firstName:
          type: string
          nullable: true
          description: The first name of the user.
          maxLength: 35
        middleName:
          type: string
          nullable: true
          description: The middle name of the user.
          maxLength: 35
        lastName:
          type: string
          nullable: true
          description: The last name of the user.
          maxLength: 35
        company:
          type: string
          nullable: true
          description: The company of the user.
          maxLength: 100
        userName:
          type: string
          nullable: true
          description: The username of the user.
          pattern: ^[a-zA-Z0-9_.-]+$
          maxLength: 35
        displayName:
          type: string
          nullable: true
          description: The display name of the user.
          maxLength: 50
        avatarId:
          type: string
          format: uuid
          nullable: true
          description: ID of the user's avatar asset.
        avatar:
          description: The populated avatar Asset (or its ID when not populated).
          oneOf:
            - type: string
              format: uuid
            - $ref: '#/components/schemas/Asset'
        color:
          type: string
          description: Hex color assigned to the user, used for avatars and cursors.
          default: '#ffffff'
        email:
          type: string
          format: email
          description: The email address of the user.
        isEmailVerified:
          type: boolean
          default: false
          description: Indicates whether the user's email is verified.
        hasPassword:
          type: boolean
          default: false
          description: False for accounts created via an OAuth provider that have not set a password.
        allowOauthAutolink:
          type: boolean
          default: true
          description: Whether an OAuth sign-in with a matching verified email may be linked to this account automatically.
        mfaEnabled:
          type: boolean
          default: false
          description: Indicates whether multi-factor authentication (MFA) is enabled for the user.
        mfaType:
          type: string
          enum:
            - totp
          default: totp
          description: The type of multi-factor authentication (MFA) used by the user.
        status:
          type: string
          enum:
            - active
            - inactive
            - locked
            - pendingDelete
          default: active
          description: The status of the user.
        accountType:
          type: string
          enum:
            - standard
            - guest
          default: standard
          description: Guest accounts are created from public release comment flows and can be upgraded to standard accounts.
        globalRoles:
          type: array
          items:
            type: string
          description: Platform-wide roles (e.g. staff).
        preferences:
          type: object
          description: The user's preferences (hide, emailNotifications, systemNotifications, sounds, dateFormat, ...).
          additionalProperties: true
        language:
          type: string
          default: en
        externalIds:
          type: object
          description: IDs of the user in external systems (e.g. the payment provider customer ID).
          additionalProperties: true
        lastSeen:
          type: string
          format: date-time
          nullable: true
          description: When the user was last active.
        hasUsedTrial:
          type: boolean
          default: false
        hasUsedDemo:
          type: boolean
          default: false
        hasSeen:
          type: array
          items:
            type: string
          description: One-time UI elements the user has already seen (e.g. welcomeVideo).
        awards:
          type: array
          items:
            type: string
            enum:
              - staff
              - earlyAdopter
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - email
        - status
        - accountType
    PublicUser:
      type: object
      description: The subset of a user's profile visible to other users.
      properties:
        id:
          type: string
          format: uuid
          description: User's Id
        firstName:
          type: string
          nullable: true
          description: The first name of the user.
          maxLength: 35
        lastName:
          type: string
          nullable: true
          description: The last name of the user.
          maxLength: 35
        displayName:
          type: string
          nullable: true
          description: The display name of the user.
          maxLength: 50
        avatar:
          description: The populated avatar Asset (or its ID when not populated).
          oneOf:
            - type: string
              format: uuid
            - $ref: '#/components/schemas/Asset'
        color:
          type: string
          description: Hex color assigned to the user.
        awards:
          type: array
          items:
            type: string
            enum:
              - staff
              - earlyAdopter
        accountType:
          type: string
          enum:
            - standard
            - guest
      required:
        - id
    Error:
      type: object
      properties:
        type:
          type: string
        code:
          type: number
        message:
          type: string
    CursorPaginatedResult:
      type: object
      properties:
        nextCursor:
          type: string
          nullable: true
        prevCursor:
          type: string
          nullable: true
        hasNextPage:
          type: boolean
        hasPrevPage:
          type: boolean
        totalResults:
          type: integer
          description: >-
            Total number of results matching the query (may be approximate or unavailable depending on pagination
            settings).
          nullable: true
        totalPreviousResults:
          type: integer
          description: Estimated number of results before the current page.
          nullable: true
        totalNextResults:
          type: integer
          description: Estimated number of results after the current page.
          nullable: true
        queryAt:
          type: integer
          description: Timestamp of when the query was executed.
      required:
        - hasNextPage
        - hasPrevPage
    CursorPaginatedFeed:
      allOf:
        - $ref: '#/components/schemas/CursorPaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                allOf:
                  - $ref: '#/components/schemas/Asset'
                  - properties:
                      chat:
                        $ref: '#/components/schemas/Chat'
    PaginatedResult:
      type: object
      properties:
        page:
          type: integer
          description: The current page number.
        limit:
          type: integer
          description: The maximum number of results per page.
        totalPages:
          type: integer
          description: The total number of pages.
        totalResults:
          type: integer
          description: The total number of results across all pages.
        queryAt:
          type: integer
          description: Timestamp of query.
      required:
        - page
        - limit
        - totalPages
        - totalResults
    Tags:
      type: array
      items:
        $ref: '#/components/schemas/Tag'
    Tag:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: The unique identifier of the tag
        name:
          type: string
          description: The display name of the tag
          maxLength: 50
        slug:
          type: string
          description: URL-friendly version of the tag name
        ownerResourceType:
          type: string
          enum:
            - project
          description: The type of resource that owns this tag (currently limited to project only)
        ownerResourceId:
          type: string
          format: uuid
          description: The ID of the resource that owns this tag
        color:
          type: string
          description: Hex color code for the tag from predefined color list
          enum:
            - '#37474F'
            - '#FF5722'
            - '#2962FF'
            - '#33691E'
            - '#00796B'
            - '#455A64'
            - '#2979FF'
            - '#827717'
            - '#7986CB'
            - '#8E24AA'
            - '#9575CD'
            - '#BF360C'
            - '#01579B'
            - '#EF6C00'
            - '#AA00FF'
            - '#F44336'
            - '#7C4DFF'
            - '#E65100'
            - '#8D6E63'
            - '#283593'
            - '#607D8B'
            - '#009688'
            - '#FF5252'
            - '#03A9F4'
            - '#C2185B'
            - '#00ACC1'
            - '#E91E63'
            - '#5D4037'
            - '#78909C'
            - '#1E88E5'
            - '#D500F9'
            - '#7E57C2'
            - '#5C6BC0'
            - '#558B2F'
            - '#2E7D32'
            - '#F50057'
            - '#004D40'
            - '#0D47A1'
            - '#C51162'
            - '#D50000'
            - '#6200EA'
            - '#00BCD4'
            - '#0277BD'
        createdAt:
          type: string
          format: date-time
          description: The timestamp when the tag was created
          example: '2023-03-15T12:00:00.000Z'
        updatedAt:
          type: string
          format: date-time
          description: The timestamp when the tag was last updated
          example: '2023-03-15T12:00:00.000Z'
      required:
        - id
        - name
        - slug
        - ownerResourceType
        - ownerResourceId
        - color
    Config:
      type: object
      properties:
        limits:
          type: object
          properties:
            chat:
              type: object
              properties:
                maxSubjectCharacterLength:
                  type: integer
                  description: Maximum character length for chat subjects.
                maxContentCharacterLength:
                  type: integer
                  description: Maximum character length for chat message content.
                numberOfRecentMessages:
                  type: integer
                  description: Number of recent messages to retrieve for a chat.
                numberOfRecentReplies:
                  type: integer
                  description: Number of recent replies to retrieve for a message.
                maxMessageResults:
                  type: integer
                  description: Maximum number of messages to return in a single query.
                maxReplyResults:
                  type: integer
                  description: Maximum number of replies to return in a single query.
                maxMemberChats:
                  type: integer
                  description: Maximum number of member chats a user can participate in.
                maxMemberChatRecentMessages:
                  type: integer
                  description: Maximum number of recent messages to retrieve for member chats.
            chatMessage:
              type: object
              properties:
                maxNumberOfAttachments:
                  type: integer
                  description: Maximum number of attachments allowed per chat message.
                maxNumberOfAnnotations:
                  type: integer
                  description: Maximum number of annotations allowed per chat message.
                maxNumberOfMentions:
                  type: integer
                  description: Maximum number of user mentions allowed per chat message.
            asset:
              type: object
              properties:
                maxNameLength:
                  type: integer
                  description: Maximum character length for asset names.
                maxDownloadResults:
                  type: integer
                  description: Maximum number of assets that can be downloaded in a single request.
                maxRepairResults:
                  type: integer
                  description: Maximum number of assets that can be repaired in a single request.
                maxPublishResults:
                  type: integer
                  description: Maximum number of assets that can be published in a single request.
            membership:
              type: object
              properties:
                maxMembershipRecords:
                  type: integer
                  description: Maximum number of membership records allowed.
            project:
              type: object
              properties:
                maxProjects:
                  type: integer
                  description: Maximum number of projects a user can create or be a member of.
                maxNameLength:
                  type: integer
                  description: Maximum character length for project names.
                maxDescriptionLength:
                  type: integer
                  description: Maximum character length for project descriptions.
                maxFeedResults:
                  type: integer
                  description: Maximum number of results to return in the project feed.
            workspace:
              type: object
              properties:
                maxWorkspacesPerUser:
                  type: integer
                  description: Maximum number of workspaces a user can be associated with.
                maxNameLength:
                  type: integer
                  description: Maximum character length for workspace names.
                maxDescriptionLength:
                  type: integer
                  description: Maximum character length for workspace descriptions.
            folder:
              type: object
              properties:
                maxNameLength:
                  type: integer
                  description: Maximum character length for folder names.
                maxDescriptionLength:
                  type: integer
                  description: Maximum character length for folder descriptions.
                maxFolders:
                  type: integer
                  description: The maximum number of folders a user can have.
            user:
              type: object
              properties:
                minPasswordLength:
                  type: integer
                  description: Minimum required length for user passwords.
                minUserNameLength:
                  type: integer
                  description: Minimum required length for usernames.
                maxUserNameLength:
                  type: integer
                  description: Maximum allowed length for usernames.
                maxFirstNameLength:
                  type: integer
                  description: Maximum allowed length for user first names.
                maxMiddleNameLength:
                  type: integer
                  description: Maximum allowed length for user middle names.
                maxLastNameLength:
                  type: integer
                  description: Maximum allowed length for user last names.
                maxCompanyNameLength:
                  type: integer
                  description: Maximum allowed length for company names.
                maxDisplayNameLength:
                  type: integer
                  description: Maximum allowed length for display names.
            notification:
              type: object
              properties:
                maxNotificationResults:
                  type: integer
                  description: Maximum number of notifications to return in a single query.
                maxNumberOfChannels:
                  type: integer
                  description: Maximum number of channels to retrieve notification results for.
            task:
              type: object
              properties:
                maxTaskResults:
                  type: integer
                  description: Maximum number of tasks to return in a single query.
            subscription:
              type: object
              properties:
                maxStarterProducts:
                  type: integer
                  description: Maximum number of starter products allowed per subscription.
            summary:
              type: object
              properties:
                maxSummaryResults:
                  type: integer
                  description: Maximum number of summaries to return in a single query.
            maxAssets:
              type: integer
              description: The maximum number of assets a user can have.
            tag:
              type: object
              properties:
                maxNameLength:
                  type: integer
                  description: Maximum character length for tag names.
                maxTagsPerResource:
                  type: integer
                  description: Maximum number of tags allowed per resource.
        colors:
          type: object
          properties:
            approvedColors:
              type: array
              items:
                type: string
    ResourceSettings:
      type: object
      properties:
        resourceId:
          type: string
          format: uuid
        resourceType:
          type: string
          enum:
            - workspace
            - project
        emailNotifications:
          type: object
          additionalProperties: true
        systemNotifications:
          type: object
          additionalProperties: true
        sounds:
          type: object
          additionalProperties: true
      required:
        - resourceId
        - resourceType
    EffectiveSettings:
      type: object
      properties:
        emailNotifications:
          type: object
          additionalProperties: true
        systemNotifications:
          type: object
          additionalProperties: true
        sounds:
          type: object
          additionalProperties: true
    AiTone:
      type: object
      properties:
        id:
          type: string
          enum:
            - professional
            - casual
            - concise
            - friendly
            - formal
            - cleanup
        label:
          type: string
    AiBalanceResponse:
      type: object
      properties:
        balance:
          type: integer
          description: Spendable Nurama Credit balance for the workspace, in credits (`planBalance + purchasedBalance`).
        planBalance:
          type: integer
          description: Credits from the monthly plan grant. Refreshes each period; does not carry over.
        purchasedBalance:
          type: integer
          description: Credits from top-up purchases. Accumulates.
        hasPriorTopUp:
          type: boolean
          description: Whether the workspace has ever made a manual credit purchase. Precondition for enabling auto top-up.
    AiComposeResponse:
      type: object
      properties:
        text:
          type: string
          description: Nu's commentary for this turn.
        proposal:
          type: string
          nullable: true
          description: The message Nu proposes posting, or null when it has none yet.
        billedCredits:
          type: integer
        balanceAfter:
          type: integer
          nullable: true
        eventId:
          type: string
          format: uuid
          nullable: true
    AiUsageReportResponse:
      type: object
      properties:
        total:
          type: object
          properties:
            billedCredits:
              type: integer
            callCount:
              type: integer
            tokensIn:
              type: integer
            tokensOut:
              type: integer
        byUser:
          type: array
          items:
            type: object
            properties:
              userId:
                type: string
                format: uuid
              displayName:
                type: string
                nullable: true
              avatar:
                nullable: true
                description: The user's avatar asset, when they have one.
              billedCredits:
                type: integer
              callCount:
                type: integer
        byIntegration:
          type: array
          items:
            type: object
            properties:
              integrationPoint:
                type: string
                enum:
                  - polish
                  - chat
                  - taskGeneration
                  - imageRevision
                  - convo
              billedCredits:
                type: integer
              callCount:
                type: integer
        filters:
          type: object
          description: The effective filters after defaults were applied.
          properties:
            startDate:
              type: string
              format: date-time
            endDate:
              type: string
              format: date-time
            userId:
              type: string
              format: uuid
              nullable: true
            integrationPoint:
              type: string
              nullable: true
    AiPolishResponse:
      type: object
      properties:
        polishedText:
          type: string
          description: The rewritten draft. Trimmed; never empty on a 200.
        billedCredits:
          type: integer
          description: |
            Credits actually deducted for this call: the provider cost of the
            tokens used, multiplied by Nurama's markup and rounded up to a
            whole credit.
        balanceAfter:
          type: integer
          nullable: true
          description: Workspace credit balance immediately after deduction.
        eventId:
          type: string
          format: uuid
          nullable: true
          description: |
            ID of the usage event recorded for this call. May be `null` in
            the rare case where the deduction succeeded but the event could
            not be recorded.
    AiTaskGenerationResponse:
      type: object
      properties:
        tasks:
          type: array
          description: |
            Generated task drafts. Always at least one entry on a 200 (a
            result with zero usable tasks is reported as a 502
            `aiTaskGenerationInvalidOutput`). At most 10 entries.
          items:
            type: object
            properties:
              subject:
                type: string
                description: Short imperative title (≤500 chars after trimming).
              description:
                type: string
                description: Optional expansion (≤20000 chars after trimming); may be empty.
        billedCredits:
          type: integer
          description: |
            Credits actually deducted for this call. Same accounting model
            as `AiPolishResponse.billedCredits`.
        balanceAfter:
          type: integer
          nullable: true
          description: Workspace credit balance immediately after deduction.
        eventId:
          type: string
          format: uuid
          nullable: true
          description: ID of the usage event recorded for this call.
    AiChatTopic:
      type: object
      description: One AI chat topic. Private to its `creatorId`.
      properties:
        id:
          type: string
          format: uuid
        chatType:
          type: string
          enum:
            - ai
        scopeType:
          type: string
          enum:
            - workspace
            - project
            - social
        scopeId:
          type: string
          format: uuid
          nullable: true
          description: Null when `scopeType` is `social`.
        billedResourceType:
          type: string
          enum:
            - workspace
            - user
          description: '`workspace` for AI chat topics; `user` for support topics.'
        billedResourceId:
          type: string
          format: uuid
          description: The billing workspace id. Always set, even for social topics.
        creatorId:
          type: string
          format: uuid
        chatId:
          type: string
          format: uuid
          description: >-
            ID of the chat that holds the topic's messages. Use it with `GET /v1/chats/{chatId}/messages` and `POST
            /v1/chats/{chatId}/new-message`.
        title:
          type: string
          nullable: true
          description: Null until the assistant auto-names the topic or the user sets one.
        titleGenerationFailed:
          type: boolean
        totalMessages:
          type: integer
        totalBilledCredits:
          type: integer
        lastMessagePreview:
          type: string
          nullable: true
          description: Truncated plain-text snapshot of the most recent message.
        lastMessageAt:
          type: string
          format: date-time
          nullable: true
        contextItems:
          type: array
          nullable: true
          description: Context-column pins (`{ type, id }`), see `PATCH /ai/chat/topics/{topicId}`.
          items:
            type: object
            properties:
              type:
                type: string
                enum:
                  - asset
                  - folder
                  - user
                  - submission
                  - public
              id:
                type: string
        archived:
          type: boolean
        status:
          type: string
          enum:
            - active
            - pendingDelete
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    AiChatTopicListResponse:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/AiChatTopic'
    AiChatCreateTopicResponse:
      type: object
      description: |
        Newly-created (always-empty) topic. Send the first message via
        POST /v1/chats/{topic.chatId}/new-message.
      properties:
        topic:
          $ref: '#/components/schemas/AiChatTopic'
    Board:
      type: object
      properties:
        id:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        creatorId:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
        visibility:
          type: array
          items:
            type: string
            enum:
              - creator
              - reviewer
        status:
          type: string
          enum:
            - active
            - archived
            - pendingDelete
        pendingDeleteAt:
          type: string
          format: date-time
          nullable: true
          description: Set when `status` is `pendingDelete`; the time after which the cleanup pass may hard-delete the board.
        followers:
          type: array
          items:
            type: string
            format: uuid
          description: User IDs following this board
        tags:
          type: array
          items:
            type: string
            format: uuid
          description: Tag IDs applied to this board (project-scoped tags)
        sortOrder:
          type: number
        inheritanceId:
          type: string
          format: uuid
        columns:
          type: array
          items:
            $ref: '#/components/schemas/BoardColumn'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    BoardWithTasks:
      allOf:
        - $ref: '#/components/schemas/Board'
        - type: object
          properties:
            columns:
              type: array
              items:
                allOf:
                  - $ref: '#/components/schemas/BoardColumn'
                  - type: object
                    properties:
                      tasks:
                        type: array
                        items:
                          $ref: '#/components/schemas/Task'
                      taskCount:
                        type: integer
    BoardColumn:
      type: object
      properties:
        id:
          type: string
          format: uuid
        boardId:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        color:
          type: string
          nullable: true
          description: Hex color code
        isDefault:
          type: boolean
        taskStatus:
          type: string
          nullable: true
          enum:
            - pending
            - inProgress
            - complete
            - closed
          description: Status applied to tasks moved into this column
        reviewersCanContribute:
          type: boolean
        sortOrder:
          type: number
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    TaskLink:
      type: object
      properties:
        id:
          type: string
          format: uuid
        taskId:
          type: string
          format: uuid
        linkedTaskId:
          type: string
          format: uuid
        linkType:
          type: string
          enum:
            - related
            - blocks
            - blockedBy
            - duplicate
        linkedTask:
          $ref: '#/components/schemas/Task'
        createdAt:
          type: string
          format: date-time
    BotUser:
      type: object
      description: Bot user profile. Authentication fields are never included.
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
        color:
          type: string
        accountType:
          type: string
          enum:
            - bot
        status:
          type: string
        avatarId:
          type: string
          format: uuid
          nullable: true
        avatar:
          nullable: true
          description: The bot's avatar asset, or null.
          allOf:
            - $ref: '#/components/schemas/Asset'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    BotApiKeySummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        keyPrefix:
          type: string
          description: 8-char plaintext lookup prefix (the part after `nrm_bot_` and before the secret)
        name:
          type: string
        kind:
          type: string
          nullable: true
          example: botAccess
        scopes:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
    BotProjectMembership:
      type: object
      properties:
        membershipId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        projectName:
          type: string
        roles:
          type: array
          items:
            type: string
            enum:
              - creator
              - reviewer
              - projectAdmin
    MentionableSubmission:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/UUID'
        subject:
          type: string
        description:
          type: string
          nullable: true
        version:
          type: string
          nullable: true
        status:
          type: string
        totalMessages:
          type: integer
        lastMessageAt:
          type: integer
          format: int64
          nullable: true
        publishedAt:
          type: integer
          format: int64
        chatId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: Same as `id` - the submission row is also its chat.
        creator:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/PublicUser'
    MentionablePublic:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/UUID'
        token:
          type: string
          description: Share token used in `{{publicMention:token}}`.
        title:
          type: string
        description:
          type: string
          nullable: true
        status:
          type: string
        expiresAt:
          type: integer
          format: int64
          nullable: true
        createdAt:
          type: integer
          format: int64
        chatId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          nullable: true
          description: ID of the release's public topic chat, if one exists.
        creator:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/PublicUser'
    WorkspaceProjectChat:
      type: object
      properties:
        project:
          type: object
          properties:
            id:
              $ref: '#/components/schemas/UUID'
            name:
              type: string
            logo:
              nullable: true
              allOf:
                - $ref: '#/components/schemas/Asset'
            color:
              type: string
              nullable: true
        visibility:
          type: string
          enum:
            - creator
            - reviewer
        chat:
          type: object
          properties:
            id:
              $ref: '#/components/schemas/UUID'
            topicType:
              type: string
              enum:
                - project
            topicId:
              $ref: '#/components/schemas/UUID'
            visibility:
              type: string
              enum:
                - creator
                - reviewer
            participants:
              type: array
              items:
                $ref: '#/components/schemas/UUID'
            totalMessages:
              type: integer
            recentMessages:
              type: array
              maxItems: 1
              description: The latest message in the chat, if any.
              items:
                $ref: '#/components/schemas/ChatMessage'
            createdAt:
              type: integer
              format: int64
            updatedAt:
              type: integer
              format: int64
    ProjectScopeResponse:
      type: object
      properties:
        projectMembership:
          type: array
          items:
            $ref: '#/components/schemas/UserRoleResource'
        workspaceMembership:
          type: array
          items:
            $ref: '#/components/schemas/UserRoleResource'
    WorkspaceScopeResponse:
      type: array
      items:
        $ref: '#/components/schemas/UserRoleResource'
    UserRoleResource:
      type: object
      properties:
        userId:
          type: string
          example: 6661714bb26e266ece994286
        resourceId:
          type: string
          example: 6661714bb26e266ece99427f
        resourceType:
          type: string
          example: project
        roles:
          type: array
          items:
            type: string
          example:
            - projectOwner
        createdAt:
          type: integer
          example: 1717662028244
        updatedAt:
          type: integer
          example: 1717662028244
        id:
          type: string
          example: 6661714cb26e266ece99428b
        resource:
          type: object
          properties:
            workspaceId:
              type: string
              example: 6661714bb26e266ece994278
            name:
              type: string
              example: McGlynn Group
            slug:
              type: string
              example: mcglynn-group
            status:
              type: string
              example: active
            createdAt:
              type: integer
              example: 1717662027953
            updatedAt:
              type: integer
              example: 1717662027953
            id:
              type: string
              example: 6661714bb26e266ece99427f
        user:
          type: object
          properties:
            firstName:
              type: string
              example: Rory
            lastName:
              type: string
              example: Mann
            displayName:
              type: string
              example: Catherine
            id:
              type: string
              example: 6661714bb26e266ece994286
      required:
        - userId
        - resourceId
        - resourceType
        - roles
        - createdAt
        - updatedAt
        - id
    IndexPaginatedMessages:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/ChatMessage'
    CursorPaginatedMessages:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/ChatMessage'
        nextCursor:
          type: string
          nullable: true
        prevCursor:
          type: string
          nullable: true
        hasNextPage:
          type: boolean
        hasPrevPage:
          type: boolean
        totalResults:
          type: integer
        totalPreviousResults:
          type: integer
        totalNextResults:
          type: integer
    IndexPaginatedReplies:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/ChatMessage'
    CursorPaginatedReplies:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/ChatMessage'
        nextCursor:
          type: string
          nullable: true
        prevCursor:
          type: string
          nullable: true
        hasNextPage:
          type: boolean
        hasPrevPage:
          type: boolean
        totalResults:
          type: integer
        totalPreviousResults:
          type: integer
        totalNextResults:
          type: integer
    IndexPaginatedChatMembers:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/ChatMember'
    CursorPaginatedChatMembers:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/ChatMember'
        nextCursor:
          type: string
          nullable: true
        prevCursor:
          type: string
          nullable: true
        hasNextPage:
          type: boolean
        hasPrevPage:
          type: boolean
        totalResults:
          type: integer
        totalPreviousResults:
          type: integer
        totalNextResults:
          type: integer
    Convo:
      type: object
      description: >-
        A video/audio conversation. Participant, starter, chat and scope fields are populated on every route that
        returns a convo.
      properties:
        id:
          $ref: '#/components/schemas/UUID'
        convoType:
          type: string
          enum:
            - video
            - audio
        status:
          type: string
          enum:
            - active
            - completed
            - cancelled
        subject:
          type: string
          description: Optional subject. Omitted when not set.
        notes:
          type: string
          description: Optional notes. Omitted when not set.
        chatId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: ID of the chat the convo belongs to
        chat:
          description: The populated chat (Chat, ChatMember or ChatSubmission depending on `chatType`).
          oneOf:
            - $ref: '#/components/schemas/Chat'
            - $ref: '#/components/schemas/ChatMember'
            - $ref: '#/components/schemas/ChatSubmission'
        chatType:
          type: string
          enum:
            - chat
            - chatMember
            - chatSubmission
          description: Internal chat model name (request bodies use `topic` / `member` / `submission`).
        messageId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: ID of the system message created when the convo started
        message:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: Alias of `messageId`
        dailyRoomName:
          type: string
          description: Daily.co room name
        dailyRoomUrl:
          type: string
          description: Full URL to join the Daily.co room
        dailyRoomConfig:
          type: object
          description: Daily.co room configuration used when the room was created
        startedById:
          $ref: '#/components/schemas/UUID'
        startedBy:
          description: User who started the convo (populated)
          allOf:
            - $ref: '#/components/schemas/PublicUser'
        activeParticipants:
          type: array
          description: Users currently in the convo (populated)
          items:
            $ref: '#/components/schemas/PublicUser'
        allParticipants:
          type: array
          description: All users who have joined at any point (populated)
          items:
            $ref: '#/components/schemas/PublicUser'
        startedAt:
          type: integer
          format: int64
          description: Timestamp (ms) the convo started
        endedAt:
          type: integer
          format: int64
          nullable: true
          description: Timestamp (ms) the convo completed or was cancelled
        durationMinutes:
          type: integer
          nullable: true
          description: Duration in minutes, set on completion
        recordings:
          type: array
          description: Daily.co recordings attached to the convo
          items:
            type: object
        transcripts:
          type: array
          description: Daily.co transcripts attached to the convo
          items:
            type: object
        participantSessions:
          type: array
          description: Per-participant session records appended from Daily.co webhooks
          items:
            type: object
        billedAtVideo:
          type: integer
          format: int64
          nullable: true
        billedCreditsVideo:
          type: integer
          nullable: true
        billedAtRecording:
          type: integer
          format: int64
          nullable: true
        billedCreditsRecording:
          type: integer
          nullable: true
        inheritanceId:
          $ref: '#/components/schemas/UUID'
        scopeId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: ID of the project scope (when the chat belongs to a project)
        scopeType:
          type: string
          enum:
            - project
          nullable: true
        scopeVisibility:
          type: array
          items:
            type: string
            enum:
              - creator
              - reviewer
        createdAt:
          type: integer
          format: int64
        updatedAt:
          type: integer
          format: int64
    MembershipReportWithTotals:
      allOf:
        - $ref: '#/components/schemas/MembershipReport'
        - type: object
          properties:
            totalCreators:
              type: integer
              description: Number of matching members holding a creator role.
            totalReviewers:
              type: integer
              description: Number of matching members holding a reviewer role.
    CursorPaginatedNotifications:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/Notification'
        nextCursor:
          type: string
          nullable: true
        prevCursor:
          type: string
          nullable: true
        hasNextPage:
          type: boolean
        hasPrevPage:
          type: boolean
        totalResults:
          type: integer
        totalPreviousResults:
          type: integer
        totalNextResults:
          type: integer
    IndexPaginatedNotifications:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/Notification'
            hasNextPage:
              type: boolean
            hasPrevPage:
              type: boolean
    IndexPaginatedNewNotifications:
      allOf:
        - $ref: '#/components/schemas/IndexPaginatedNotifications'
        - type: object
          properties:
            newNotificationCount:
              type: integer
              description: >-
                Count of notifications newer than the caller's last-seen record for these channels/types (independent of
                `limit`).
            createdBefore:
              type: integer
              format: int64
              description: Timestamp (ms) used as the upper bound of the query.
            lastSeen:
              type: integer
              format: int64
              nullable: true
              description: The caller's previous last-seen timestamp for these channels/types, or null if there was no record.
            updatedLastSeen:
              type: integer
              format: int64
              nullable: true
              description: New last-seen timestamp written by this call when `updateLastSeen` is true; otherwise null.
    Submissions:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/ChatSubmission'
    CursorPaginatedChatMessagesWithChat:
      allOf:
        - $ref: '#/components/schemas/CursorPaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                allOf:
                  - $ref: '#/components/schemas/ChatMessage'
                  - type: object
                    properties:
                      chat:
                        allOf:
                          - $ref: '#/components/schemas/Chat'
                        description: Parent chat. Populated on the highlighted-messages listings.
    CursorPaginatedSubmissions:
      allOf:
        - $ref: '#/components/schemas/CursorPaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/ChatSubmission'
    AssetWithChat:
      description: >
        An asset as returned by the feed / asset listings. When chats are included, the entry of `chats` for the
        requested visibility is populated with the chat and its recent messages instead of the bare chat ID.
      allOf:
        - $ref: '#/components/schemas/Asset'
        - type: object
          properties:
            chats:
              type: object
              properties:
                creator:
                  oneOf:
                    - $ref: '#/components/schemas/UUID'
                    - $ref: '#/components/schemas/Chat'
                reviewer:
                  oneOf:
                    - $ref: '#/components/schemas/UUID'
                    - $ref: '#/components/schemas/Chat'
    Feed:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/AssetWithChat'
    SearchResult:
      type: object
      description: >
        A single hit from the project search endpoint. The `record` field carries the same populated shape the content
        type's native list view returns (Asset / ChatMessage / Task).
      required:
        - id
        - contentType
        - createdAt
        - relevance
      properties:
        id:
          $ref: '#/components/schemas/UUID'
        contentType:
          type: string
          enum:
            - asset
            - chatMessage
            - task
        title:
          type: string
          nullable: true
          description: Asset.name or Task.subject. Null for chat messages — use `snippet`.
        snippet:
          type: string
          nullable: true
          description: ts_headline excerpt with `<mark>...</mark>` highlights around matched terms. Null for assets.
        creatorId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          nullable: true
          description: Authoring user. Asset.creatorId / ChatMessage.authorId / Task.creatorId.
        chatId:
          allOf:
            - $ref: '#/components/schemas/UUID'
          nullable: true
          description: Set only on chatMessage hits — the parent chat's UUID.
        createdAt:
          type: string
          format: date-time
        relevance:
          type: number
          format: float
          description: Relevance score; higher means a better match.
        record:
          nullable: true
          description: Fully-populated record, shaped per the content type's list view.
          oneOf:
            - $ref: '#/components/schemas/Asset'
            - $ref: '#/components/schemas/ChatMessage'
            - $ref: '#/components/schemas/Task'
    SearchResults:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/SearchResult'
    UpdateLogoResponse:
      allOf:
        - $ref: '#/components/schemas/AssetAndSignedLink'
        - type: object
          properties:
            project:
              allOf:
                - $ref: '#/components/schemas/Project'
              description: The project with its `logo` reference updated.
    DeleteImpact:
      type: object
      description: |
        What a delete would reach beyond the rows named in the request. Computed from the same cascade the delete runs.
      properties:
        selectedCount:
          type: integer
          description: How many of the requested paths resolved to a row.
        affectedAssetCount:
          type: integer
          description: Distinct assets that would lose a reference.
        counts:
          type: object
          description: Reference count per collection. Only non-empty collections appear.
          properties:
            reviewer:
              type: integer
            submission:
              type: integer
            public:
              type: integer
            other:
              type: integer
        references:
          type: array
          description: >
            The secondary references that would be removed — the reviewer, submission and public-release copies. Does
            not include the rows the caller listed.
          items:
            type: object
            properties:
              fileSystemId:
                type: string
                format: uuid
              assetId:
                type: string
                format: uuid
              assetName:
                type: string
              kind:
                type: string
                enum:
                  - reviewer
                  - submission
                  - public
                  - other
              path:
                type: string
              itemPath:
                type: string
              label:
                type: string
                description: Human-readable location, e.g. "Submission / Round 2".
      required:
        - selectedCount
        - affectedAssetCount
        - counts
        - references
    PublishItemError:
      type: object
      properties:
        type:
          type: string
          description: Error code, e.g. `resourcesNotFound`, `publisherRequired`.
        message:
          type: string
        data:
          description: Error-specific payload (for example the ids that were not found).
    PublishItemsResponse:
      type: object
      properties:
        published:
          type: array
          description: The submitted items that were published (assets and folders).
          items:
            oneOf:
              - $ref: '#/components/schemas/Asset'
              - $ref: '#/components/schemas/Folder'
        totalPublished:
          type: integer
          description: Total items published, including assets inside published folders.
        errors:
          type: array
          items:
            $ref: '#/components/schemas/PublishItemError'
      required:
        - published
        - totalPublished
        - errors
    UnpublishItemsResponse:
      type: object
      properties:
        unpublished:
          type: array
          description: The assets that were unpublished.
          items:
            $ref: '#/components/schemas/Asset'
        totalUnpublished:
          type: integer
          description: Total items unpublished, including removed reviewer file-system entries.
        removedPaths:
          type: array
          description: Reviewer-tree paths whose entries were removed (submitted items only).
          items:
            type: string
        errors:
          type: array
          items:
            $ref: '#/components/schemas/PublishItemError'
      required:
        - unpublished
        - totalUnpublished
        - removedPaths
        - errors
    FileSystemItem:
      type: object
      description: A FileSystem entry — one placement of an asset or folder at a path — with its populated resource.
      properties:
        id:
          type: string
          format: uuid
        resourceId:
          type: string
          format: uuid
        resourceType:
          type: string
          enum:
            - asset
            - folder
        resourceName:
          type: string
        resourceSlug:
          type: string
          nullable: true
        resourceCreatorId:
          type: string
          format: uuid
        resourceTags:
          type: array
          items:
            type: string
            format: uuid
        resourceMediaType:
          type: string
          description: Media type of the resource (`folder` for folders).
        resourceStatus:
          type: string
          enum:
            - active
            - pendingDelete
        resourceCreatedAt:
          type: string
          format: date-time
        sizeInBytes:
          type: integer
          format: int64
        path:
          type: string
          description: Parent path of the entry, e.g. `project/{projectId}/creator/Renders`.
        itemPath:
          type: string
          nullable: true
          description: Full path of the item including its own slug.
        pathType:
          type: string
          nullable: true
          description: First segment of `path` (`project`, `submission`, `public`, ...).
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        resource:
          description: >
            The populated asset or folder. On submission listings an asset's `chats.reviewer` is populated with the
            reviewer chat; on project and public listings `chats` holds bare chat IDs.
          oneOf:
            - $ref: '#/components/schemas/Asset'
            - $ref: '#/components/schemas/Folder'
    FileSystemItems:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/FileSystemItem'
    CursorPaginatedFileSystemItems:
      allOf:
        - $ref: '#/components/schemas/CursorPaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/FileSystemItem'
    FileOperationResponse:
      type: object
      properties:
        count:
          type: integer
          description: Number of items successfully processed
          example: 1
        virtualDestination:
          type: boolean
          description: >-
            Present (true) when a move targeted a virtual folder (`Public/...`, `Submission/...`, `Review/...`) and was
            performed as a copy / publish instead.
        published:
          type: boolean
          description: Present (true) when a move onto `Review/...` published the items.
        errors:
          type: array
          description: Per-item publish errors; present only when `published` is true.
          items:
            $ref: '#/components/schemas/PublishItemError'
      required:
        - count
    AddItemsToPublicFileSystemResponse:
      type: object
      properties:
        publicFileSystem:
          $ref: '#/components/schemas/PublicFileSystem'
        itemsAdded:
          type: integer
          description: Number of file-system entries copied into the release.
      required:
        - publicFileSystem
        - itemsAdded
    PublicFileSystem:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the public file system
        ownerResourceType:
          type: string
          enum:
            - project
          description: Type of the owning resource.
        ownerResourceId:
          type: string
          format: uuid
          description: ID of the owning project
        token:
          type: string
          pattern: ^[a-zA-Z0-9]{10}$
          description: Public access token
        title:
          type: string
          nullable: true
          description: Title of the public file system
        description:
          type: string
          nullable: true
          description: Description of the public file system
        inventory:
          type: object
          nullable: true
          description: Denormalized count of items in the release keyed by media type (folders count under `folder`).
          additionalProperties:
            type: integer
          example:
            image: 12
            video: 2
            folder: 3
        status:
          type: string
          enum:
            - active
            - expired
            - disabled
            - unreleased
            - error
          description: >-
            Status of the public file system. `unreleased` is a staged release that isn't yet externally accessible;
            `error` is transient while the release's items are being copied.
        expires:
          type: string
          format: date-time
          nullable: true
          description: Expiration date and time of the public file system, or `null` when it never expires.
        hideCreators:
          type: boolean
          description: >
            Whether authorship is repressed on the external public API. When `true`, the `/public/{token}*` endpoints
            omit both this record's `creator` and every listed item's creator.
        allowAnonymousComments:
          type: boolean
          description: Whether unauthenticated visitors may comment on this release's chats by supplying a display name.
        creatorId:
          type: string
          format: uuid
        creator:
          allOf:
            - $ref: '#/components/schemas/User'
          description: >
            User who created the public file system ("Shared by"). Omitted from the external `/public/{token}` response
            when `hideCreators` is true.
        createdAt:
          type: string
          format: date-time
          description: Creation date and time
        updatedAt:
          type: string
          format: date-time
          description: Last update date and time
      required:
        - id
        - ownerResourceType
        - ownerResourceId
        - token
        - status
        - hideCreators
        - allowAnonymousComments
        - creatorId
        - createdAt
        - updatedAt
    PublicFileSystems:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/PublicFileSystem'
    CursorPaginatedPublicFileSystems:
      allOf:
        - $ref: '#/components/schemas/CursorPaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/PublicFileSystem'
    PublicAuditAsset:
      type: object
      description: A project asset that is or has been publicly exposed.
      properties:
        id:
          type: string
          format: uuid
          description: Asset ID.
        name:
          type: string
          description: Asset name.
        mediaType:
          type: string
          description: Media type of the asset (e.g., `image`, `video`).
        thumbnail:
          nullable: true
          type: object
          description: Thumbnail file reference, or `null` when no thumbnail is available.
          properties:
            keyPath:
              type: string
              description: Storage key path for the thumbnail file.
        creator:
          allOf:
            - $ref: '#/components/schemas/User'
          nullable: true
          description: User who created the asset, populated with public profile fields and avatar.
        publicFileSystems:
          type: array
          description: All public file systems that have included this asset (ordered by most recent first).
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              title:
                type: string
              status:
                type: string
                enum:
                  - active
                  - expired
                  - disabled
                  - unreleased
                  - error
            required:
              - id
              - title
              - status
        publicLinkCount:
          type: integer
          description: All-time count of `PublicAssetLink` records for this asset, regardless of status.
        hasActivePublicLink:
          type: boolean
          description: Denormalized flag — `true` when the asset has at least one active public download link.
        everPublic:
          type: boolean
          description: One-way flag — `true` once the asset has been publicly exposed at any point.
        createdAt:
          type: string
          format: date-time
          description: When the asset was created.
      required:
        - id
        - name
        - mediaType
        - thumbnail
        - creator
        - publicFileSystems
        - publicLinkCount
        - hasActivePublicLink
        - everPublic
        - createdAt
    PublicAuditAssets:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/PublicAuditAsset'
    PublicCursorPaginatedFileSystemItems:
      description: >-
        Cursor-paginated file-system items (returned when `paginate=cursor`). Item shape matches
        `FileSystemItems.results[]`.
      allOf:
        - $ref: '#/components/schemas/CursorPaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                type: object
                properties:
                  resourceId:
                    type: string
                    format: uuid
                  resourceType:
                    type: string
                    enum:
                      - asset
                      - folder
                  resourceName:
                    type: string
                  path:
                    type: string
                  resourceCreatedAt:
                    type: string
                    format: date-time
                  resource:
                    oneOf:
                      - $ref: '#/components/schemas/Asset'
                      - $ref: '#/components/schemas/Folder'
    PublicAssetLink:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the public asset link.
        token:
          type: string
          pattern: ^[a-zA-Z0-9]{10}$
          description: The 10-character alphanumeric download token.
          example: Ab3dEf9HiJ
        assetId:
          type: string
          format: uuid
          description: ID of the linked asset.
        projectId:
          type: string
          format: uuid
          description: ID of the project the asset belongs to.
        creatorId:
          type: string
          format: uuid
          description: ID of the user who created the link.
        expires:
          type: string
          format: date-time
          nullable: true
          description: Expiration timestamp. Null if the link does not expire.
        status:
          type: string
          enum:
            - active
            - disabled
            - expired
          description: Current status of the link.
        mode:
          type: string
          enum:
            - download
            - embed
            - embed-download
          description: |
            Capability mode for the link.
            - `download` — only the `/download` endpoint is allowed.
            - `embed` — only the `/embed-files` endpoint is allowed.
            - `embed-download` — both endpoints are allowed (default; backward-compatible).
          example: embed-download
        publicUrl:
          type: string
          description: The full public download page URL.
          example: https://app.nurama.com/public-download/Ab3dEf9HiJ
        isExpired:
          type: boolean
          description: Whether the link has passed its expiration date.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - token
        - assetId
        - projectId
        - creatorId
        - status
        - publicUrl
    ChartDataEntry:
      type: object
      properties:
        period:
          type: string
          description: Period label formatted for the aggregation period (UTC), e.g. `YYYY-MM-DD` for `day`.
          example: '2026-09-20'
        averageSizeInBytes:
          type: integer
          minimum: 0
    ChartDataArray:
      type: array
      items:
        $ref: '#/components/schemas/ChartDataEntry'
    StorageRecord:
      type: object
      properties:
        id:
          type: string
          format: uuid
        resourceId:
          type: string
          format: uuid
        resourceType:
          type: string
          enum:
            - user
            - workspace
            - project
            - chat
            - asset
        sizeInBytes:
          type: integer
          minimum: 0
        triggerEvent:
          type: string
          description: What produced this record, e.g. `fileUpload`, `fileDelete`, `assetDelete`, `softAudit`, `hardAudit`.
        triggerResourceId:
          type: string
          format: uuid
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    TaskRelationRecord:
      type: object
      description: A task relation row as stored.
      properties:
        id:
          type: string
          format: uuid
        taskId:
          type: string
          format: uuid
        resourceId:
          type: string
          format: uuid
        resourceType:
          type: string
          enum:
            - chat
            - chatMessage
        creatorId:
          type: string
          format: uuid
        createdAt:
          type: integer
          format: int64
        updatedAt:
          type: integer
          format: int64
    TaskRelation:
      allOf:
        - $ref: '#/components/schemas/TaskRelationRecord'
        - type: object
          properties:
            relatedBy:
              description: Public user who created the relation (falls back to the creator's ID if the user cannot be loaded)
              oneOf:
                - $ref: '#/components/schemas/PublicUser'
                - type: string
                  format: uuid
            chat:
              type: object
              nullable: true
              description: Parent chat metadata (populated for both chat and chatMessage relations)
            asset:
              type: object
              nullable: true
              description: Populated when chat.topicType is 'asset'
              properties:
                id:
                  type: string
                  format: uuid
                name:
                  type: string
                mediaType:
                  type: string
                files:
                  type: array
                  items:
                    type: object
            submission:
              type: object
              nullable: true
              description: Populated when chat.chatType is 'submission'
              properties:
                id:
                  type: string
                  format: uuid
                subject:
                  type: string
            publicLink:
              type: object
              nullable: true
              description: Populated when the chat belongs to a public release
              properties:
                id:
                  type: string
                  format: uuid
                name:
                  type: string
            message:
              type: object
              nullable: true
              description: Populated when resourceType is 'chatMessage'
              properties:
                id:
                  type: string
                  format: uuid
                chatId:
                  type: string
                  format: uuid
                content:
                  type: string
                authorId:
                  type: string
                  format: uuid
                  nullable: true
                author:
                  nullable: true
                  allOf:
                    - $ref: '#/components/schemas/PublicUser'
                mentions:
                  type: array
                  items:
                    $ref: '#/components/schemas/PublicUser'
                assetMentions:
                  type: array
                  items:
                    type: object
                folderMentions:
                  type: array
                  items:
                    type: object
                createdAt:
                  type: integer
                  format: int64
    PaginatedTaskRelations:
      allOf:
        - $ref: '#/components/schemas/PaginatedResult'
        - type: object
          properties:
            results:
              type: array
              items:
                $ref: '#/components/schemas/TaskRelation'
            hasNextPage:
              type: boolean
            hasPrevPage:
              type: boolean
    TaskEvent:
      type: object
      properties:
        id:
          type: string
          format: uuid
        taskId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        actorId:
          type: string
          format: uuid
        eventType:
          type: string
          enum:
            - created
            - addedToBoard
            - moved
            - assigned
            - unassigned
            - updated
            - removedFromBoard
            - linked
            - unlinked
            - followed
            - unfollowed
          description: Event type. Intended as the key for a localised message template.
        detail:
          type: object
          description: Structured context for rendering the event (e.g. column names). Contents vary by `eventType`.
          additionalProperties: true
          example:
            toColumnName: In Progress
            fromColumnName: Backlog
        actor:
          type: object
          description: Public user who performed the action (with avatar)
          properties:
            id:
              type: string
              format: uuid
            displayName:
              type: string
            firstName:
              type: string
            lastName:
              type: string
            color:
              type: string
            avatar:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                name:
                  type: string
                files:
                  type: array
                  items:
                    type: object
                    properties:
                      functionType:
                        type: string
                      keyPath:
                        type: string
        createdAt:
          type: string
          format: date-time
    PaginatedTaskEvents:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/TaskEvent'
        page:
          type: integer
        limit:
          type: integer
        totalPages:
          type: integer
        totalResults:
          type: integer
        hasNextPage:
          type: boolean
        hasPrevPage:
          type: boolean
    CursorPaginatedTasks:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/Task'
        nextCursor:
          type: string
          description: Cursor to get the next page of results
        prevCursor:
          type: string
          description: Cursor to get the previous page of results
        hasNextPage:
          type: boolean
          description: Whether there are more results after this page
        hasPrevPage:
          type: boolean
          description: Whether there are more results before this page
        counts:
          type: object
          description: Only included when includeCounts=true
          properties:
            totalDocs:
              type: number
              description: Total number of documents matching the query
            totalDocsAfter:
              type: number
              description: Total number of documents after the current page
            totalDocsBefore:
              type: number
              description: Total number of documents before the current page
    WebhookEventName:
      type: string
      enum:
        - task.created
        - task.updated
        - task.deleted
        - chat.message.created
        - asset.published
        - webhook.test
    WebhookSubscription:
      type: object
      description: A webhook subscription. The signing secret is never included.
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        appId:
          type: string
          format: uuid
          nullable: true
          description: Reserved for the future OAuth/Apps track; always null today.
        name:
          type: string
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventName'
        status:
          type: string
          enum:
            - active
            - paused
            - failedOut
        failedOutAt:
          type: string
          format: date-time
          nullable: true
        failedOutReason:
          type: string
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
        createdById:
          type: string
          format: uuid
        lastDeliveryAt:
          type: string
          format: date-time
          nullable: true
        lastSuccessAt:
          type: string
          format: date-time
          nullable: true
        lastFailureAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    WebhookAttempt:
      type: object
      description: One delivery attempt of one notification to one subscription.
      properties:
        id:
          type: string
          format: uuid
        notificationId:
          type: string
          format: uuid
        subscriptionId:
          type: string
          format: uuid
        attempt:
          type: integer
          description: 1-based attempt number within the retry sequence.
        maxAttempts:
          type: integer
          default: 8
        status:
          type: string
          enum:
            - pending
            - inflight
            - succeeded
            - failed
            - dlq
        signatureV1:
          type: string
          description: Signature value sent (or to be recomputed) for this attempt. Debugging aid.
        scheduledAt:
          type: string
          format: date-time
        startedAt:
          type: string
          format: date-time
          nullable: true
        deliveredAt:
          type: string
          format: date-time
          nullable: true
        nextRetryAt:
          type: string
          format: date-time
          nullable: true
        responseCode:
          type: integer
          nullable: true
        responseBody:
          type: string
          nullable: true
        responseHeaders:
          type: object
          nullable: true
          additionalProperties: true
        errorMessage:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    WorkspaceWithRoles:
      allOf:
        - $ref: '#/components/schemas/Workspace'
        - type: object
          properties:
            roles:
              type: array
              description: The calling user's roles on this workspace.
              items:
                type: string
              example:
                - workspaceOwner
            capabilities:
              type: array
              description: >-
                Capability tokens available to the workspace under its current plan and opt-in settings (e.g. `boards`,
                `ai`, `convos`, `publicSharing`). Empty when the workspace has no active subscription. Not present on
                the create response.
              items:
                type: string
            settings:
              type: object
              additionalProperties: true
              description: Workspace settings JSON (see `PUT /workspaces/{workspaceId}/settings/{name}`).
    WorkspaceImageUploadResponse:
      allOf:
        - $ref: '#/components/schemas/AssetAndSignedLink'
        - type: object
          properties:
            workspace:
              $ref: '#/components/schemas/Workspace'
    IdNameOrScratchIdNameUnion:
      oneOf:
        - type: object
          properties:
            id:
              type: integer
              description: Client-side counter used to match request items with `attachmentData` items.
              example: 1
            name:
              type: string
              description: File name with extension.
              example: photo.jpg
            sizeInMB:
              type: number
              example: 1.2
            checksum:
              type: string
              description: MD5 or SHA-256 hash of the file.
          required:
            - id
            - name
            - sizeInMB
            - checksum
          additionalProperties: false
        - type: object
          properties:
            scratchId:
              type: string
              format: uuid
              description: ID of the staged Scratch object to promote.
            name:
              type: string
              description: Optional display name for the promoted asset.
          required:
            - scratchId
          additionalProperties: false
    SharedColorBlurOffsetX:
      type: object
      properties:
        color:
          type: string
          description: Hex color code (e.g.,
          default: rgba(0,0,0,0.3)
        blur:
          type: number
          minimum: 0
          maximum: 50
          description: Shadow blur radius in pixels
          default: 5
        offsetX:
          type: number
          minimum: -10
          maximum: 10
          description: Shadow X offset as percentage of media width
          default: 0.2
        offsetY:
          type: number
          minimum: -10
          maximum: 10
          description: Shadow Y offset as percentage of media height
          default: 0.2
      additionalProperties: false
    SharedUrlTitleDescription:
      type: object
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
          description: The original URL.
        title:
          type: string
          maxLength: 200
          nullable: true
          description: Page title from og:title or <title> tag.
        description:
          type: string
          maxLength: 500
          nullable: true
          description: Page description from og:description or meta description.
        image:
          type: string
          format: uri
          maxLength: 2048
          nullable: true
          description: Image URL from og:image.
        siteName:
          type: string
          maxLength: 100
          nullable: true
          description: Site name from og:site_name.
        favicon:
          type: string
          format: uri
          maxLength: 2048
          nullable: true
          description: Favicon URL.
        signature:
          type: string
          minLength: 64
          maxLength: 64
          description: HMAC-SHA256 signature for anti-spoofing verification (transient, not persisted).
      required:
        - url
      additionalProperties: false
      description: Rich link preview metadata extracted from a URL (Open Graph / meta tags).
    SharedIdFirstNameLastName:
      type: object
      properties:
        id:
          type: number
          enum:
            - 1
            - -1
          description: Sort by membership ID (1 for ascending, -1 for descending).
        firstName:
          type: number
          enum:
            - 1
            - -1
          description: Sort by user's first name (1 for ascending, -1 for descending).
        lastName:
          type: number
          enum:
            - 1
            - -1
          description: Sort by user's last name (1 for ascending, -1 for descending).
        displayName:
          type: number
          enum:
            - 1
            - -1
          description: Sort by user's display name (1 for ascending, -1 for descending).
        createdAt:
          type: number
          enum:
            - 1
            - -1
          description: Sort by membership creation date (1 for ascending, -1 for descending).
        updatedAt:
          type: number
          enum:
            - 1
            - -1
          description: Sort by membership update date (1 for ascending, -1 for descending).
        isBillable:
          type: number
          enum:
            - 1
            - -1
          description: Sort by billable status (1 for ascending, -1 for descending).
      additionalProperties: false
      description: Sorting criteria for the membership records.
    Shared5fee7073:
      type: array
      items:
        type: string
        enum:
          - test
          - notificationUpdate
          - workspaceCreate
          - workspaceUpdate
          - workspaceDelete
          - workspaceLogoUpdate
          - projectCreate
          - projectUpdate
          - projectDelete
          - projectLogoUpdate
          - projectAssetsPublish
          - projectAssetsUnpublish
          - projectItemsPublish
          - projectItemsUnpublish
          - projectGroupItemPublish
          - assetNameChange
          - assetPublish
          - assetUnpublish
          - assetTag
          - assetUntag
          - publicAssetLinkCreate
          - assetDelete
          - assetStatusUpdate
          - assetPostProcessUpdate
          - assetFileUpdate
          - assetGroupUploadComplete
          - uploadPushSummary
          - publishPushSummary
          - submissionPushSummary
          - memberJoinPush
          - chatTopicChatCreate
          - chatMemberChatCreate
          - chatUpdateSubject
          - chatMemberUpdate
          - chatDelete
          - chatMemberDelete
          - chatMemberArchive
          - chatMemberUnarchive
          - chatCreateMessage
          - chatReviseMessage
          - chatRefreshMessage
          - chatMention
          - chatDeleteMessage
          - chatRemoveAttachment
          - chatHighlightMessage
          - chatFollow
          - chatUnfollow
          - folderCreate
          - folderUpdate
          - folderDelete
          - folderPublish
          - folderTag
          - folderUntag
          - inviteCreate
          - inviteCancel
          - inviteAccept
          - membershipDelete
          - membershipAddRole
          - membershipRemoveRole
          - membershipLeaveResource
          - workspaceStorageLimitWarning
          - userEmailVerify
          - userSelfUpdate
          - userPublicUpdate
          - userDeleted
          - userAvatarUpdate
          - botCreate
          - webhookTest
          - logoUpdate
          - iconUpdate
          - subscriptionCreate
          - subscriptionUpdate
          - taskAcknowledged
          - taskStatusUpdate
          - taskCreate
          - taskFollow
          - taskUnfollow
          - notificationUpdateLastSeen
          - submissionCreate
          - submissionUpdate
          - submissionTag
          - submissionUntag
          - aiChatTopicCreate
          - aiChatTopicUpdate
          - aiChatMessageCreate
          - tagCreate
          - tagUpdate
          - tagDelete
          - fileSystemCreate
          - fileSystemMove
          - fileSystemCopy
          - fileSystemDelete
          - fileSystemPublish
          - fileSystemUnpublish
          - publicFileSystemCreate
          - publicFileSystemUpdate
          - publicFileSystemDelete
          - settingsUpdate
          - convoStart
          - convoJoin
          - convoLeave
          - convoComplete
          - convoUpdate
          - convoDelete
          - convoParticipantJoined
          - convoParticipantLeft
          - convoHandover
          - boardCreate
          - boardUpdate
          - boardDelete
          - boardTaskCreate
          - boardTaskMove
          - boardTaskAssign
          - boardColumnAdd
          - boardColumnUpdate
          - boardColumnDelete
          - boardColumnReorder
          - boardTaskAdd
          - boardTaskRemove
          - boardTaskUpdate
          - boardTaskLink
          - boardTaskUnlink
          - boardTaskRelationAdd
          - boardTaskRelationRemove
          - boardFollow
          - boardUnfollow
      uniqueItems: true
      description: >-
        Notification types to include (notification `type` names, e.g. `chatMessageCreate`; see the WebSocket event
        reference).
    SharedIdNameChecksum:
      type: object
      properties:
        id:
          type: integer
          maximum: 10
          default: 1
          description: Requester-side correlation id. Any integer up to 10.
        name:
          type: string
          pattern: ^(?!.*\.(exe|bat|com|msi|vbs|ps1|app|command|tool|sh|bin|run|jar|py|pl|rb)$).*
          description: File name including a valid extension. Executable / restricted extensions are rejected.
          example: logo.png
        checksum:
          type: string
          pattern: ^[a-f0-9]{32,64}$
          description: MD5 or SHA-256 hash of the file content.
        sizeInMB:
          type: number
          minimum: 0
          x-exclusiveMinimum: true
          description: File size in MB (positive). Used to size the signed upload.
      required:
        - name
        - checksum
        - sizeInMB
      additionalProperties: false
      description: A single logo file to upload. Unlike `UploadFile`, `basePath` is not accepted here.
    SharedNameColorBasePath:
      type: object
      properties:
        name:
          type: string
          maxLength: 100
          description: Name of the folder (HTML is sanitised). Reserved names `Review`, `Public` and `Submission` are rejected.
          example: Renders
        color:
          type: string
          enum:
            - '#37474F'
            - '#FF5722'
            - '#2962FF'
            - '#33691E'
            - '#00796B'
            - '#455A64'
            - '#2979FF'
            - '#827717'
            - '#7986CB'
            - '#8E24AA'
            - '#9575CD'
            - '#BF360C'
            - '#01579B'
            - '#EF6C00'
            - '#AA00FF'
            - '#F44336'
            - '#7C4DFF'
            - '#E65100'
            - '#8D6E63'
            - '#283593'
            - '#607D8B'
            - '#009688'
            - '#FF5252'
            - '#03A9F4'
            - '#C2185B'
            - '#00ACC1'
            - '#E91E63'
            - '#5D4037'
            - '#78909C'
            - '#1E88E5'
            - '#D500F9'
            - '#7E57C2'
            - '#5C6BC0'
            - '#558B2F'
            - '#2E7D32'
            - '#F50057'
            - '#004D40'
            - '#0D47A1'
            - '#C51162'
            - '#D50000'
            - '#6200EA'
            - '#00BCD4'
            - '#0277BD'
          description: Hex color code for the folder. Must be one of the approved colours.
        basePath:
          type: string
          description: Folder path (relative to the tree root) the folder should be created in. Omit to create at the root.
          example: Renders/2024
      additionalProperties: false
    SharedPaginateRecursiveSearchResourceIds:
      type: object
      properties:
        paginate:
          type: string
          default: index
          enum:
            - cursor
            - index
        recursiveSearch:
          type: boolean
          description: If true, includes items in every sub-folder beneath the path.
        resourceIds:
          oneOf:
            - type: array
              items:
                type: string
                format: uuid
            - type: string
              format: uuid
          default: []
          description: Filter by resource IDs. May not be combined with `resourceSlugs`.
        resourceSlugs:
          oneOf:
            - type: array
              items:
                type: string
                pattern: ^[a-z0-9-]+$
            - type: string
              pattern: ^[a-z0-9-]+$
          default: []
          description: Filter by resource slugs. May not be combined with `resourceIds`.
        resourceType:
          type: string
          enum:
            - asset
            - folder
        resourceTags:
          oneOf:
            - type: array
              items:
                type: string
                format: uuid
            - type: string
              format: uuid
          default: []
          description: Filter by tag IDs.
        creatorId:
          type: string
          format: uuid
        mediaTypes:
          oneOf:
            - type: array
              items:
                type: string
                enum:
                  - image
                  - video
                  - audio
                  - folder
                  - file
            - type: string
              enum:
                - image
                - video
                - audio
                - folder
                - file
          default: []
        resourceStatus:
          type: string
          default: active
          enum:
            - active
            - pendingDelete
        nameSearch:
          type: string
          maxLength: 100
        sort:
          type: object
          properties:
            id:
              type: number
              enum:
                - 1
                - -1
            name:
              type: number
              enum:
                - 1
                - -1
            createdAt:
              type: number
              enum:
                - 1
                - -1
            updatedAt:
              type: number
              enum:
                - 1
                - -1
            mediaType:
              type: number
              enum:
                - 1
                - -1
            status:
              type: number
              enum:
                - 1
                - -1
            sizeInBytes:
              type: number
              enum:
                - 1
                - -1
          additionalProperties: false
          default:
            id: -1
        limit:
          type: number
          minimum: 1
          maximum: 100
          default: 10
        cursor:
          type: string
          x-conditionally-required: true
        paginateReverse:
          type: boolean
          x-conditionally-required: true
        includeCounts:
          type: boolean
          x-conditionally-required: true
        includeCursorRecord:
          type: boolean
          x-conditionally-required: true
        startAt:
          type: string
          format: uuid
          x-conditionally-required: true
        includeStartAtRecord:
          type: boolean
          x-conditionally-required: true
        page:
          type: number
          minimum: 1
          x-conditionally-required: true
      additionalProperties: false
      description: >
        Body form of the file-system listing options (same fields and rules as the GET query parameters). Cursor-only
        fields are rejected unless `paginate` is `cursor`; `page` is rejected unless `paginate` is `index`.
    SharedIdNameChecksum2:
      type: object
      properties:
        id:
          type: integer
          maximum: 10
          default: 1
          description: Client-side correlation id echoed back in the response.
        name:
          type: string
          pattern: >-
            \.(jpg|jpeg|png|gif|tiff|bmp|webp|svg|heif|ico|raw|exr|heic|mp4|avi|mov|wmv|flv|mkv|webm|m4v|mpg|mpeg|rm|vob|3gp|ogv|ts|m2ts|hevc|divx)$
          description: >-
            File name including an extension of a supported image or video type (e.g. `png`, `jpg`, `gif`, `webp`,
            `mp4`, `mov`).
          example: avatar.png
        checksum:
          type: string
          pattern: ^[a-f0-9]{32,64}$
          description: MD5 (32) or SHA-256 (64) hex hash of the file content.
        sizeInMB:
          type: number
          minimum: 0
          x-exclusiveMinimum: true
          description: File size in MB; used to determine how many signed upload links to generate.
      required:
        - name
        - checksum
        - sizeInMB
      additionalProperties: false
    SharedIdNameChecksum3:
      type: object
      properties:
        id:
          type: integer
          maximum: 10
          default: 1
          description: Caller-chosen request item id, echoed back on the response.
        name:
          type: string
          pattern: >-
            \.(jpg|jpeg|png|gif|tiff|bmp|webp|svg|heif|ico|raw|exr|heic|mp4|avi|mov|wmv|flv|mkv|webm|m4v|mpg|mpeg|rm|vob|3gp|ogv|ts|m2ts|hevc|divx)$
          description: File name. Must end in a supported image or video extension.
          example: logo.png
        checksum:
          type: string
          pattern: ^[a-f0-9]{32,64}$
          description: MD5 (32 hex) or SHA-256 (64 hex) hash of the file content.
        sizeInMB:
          type: number
          minimum: 0
          x-exclusiveMinimum: true
          description: File size in MB; used to decide how many signed part-upload links to generate.
      required:
        - name
        - checksum
        - sizeInMB
      additionalProperties: false
      description: >-
        Single-file upload descriptor for a workspace logo or icon. The server creates the asset and returns signed
        upload links.
    NestedDotAnnotationInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - dot
        coordinates:
          type: object
          properties:
            x:
              type: number
              minimum: 0
              maximum: 100
              description: X coordinate as percentage (0-100)
            'y':
              type: number
              minimum: 0
              maximum: 100
              description: Y coordinate as percentage (0-100)
          required:
            - x
            - 'y'
          additionalProperties: false
          title: AnnotationCoordinatesInput
        radius:
          type: number
          minimum: 5
          maximum: 100
          default: 10
        color:
          type: string
          description: Hex color code (e.g.,
          default: '#9b9b9b'
        frame:
          type: number
          description: Video frame number (for video annotations)
          default: 0
        left:
          type: number
          minimum: 0
          maximum: 100
          description: X position as percentage of media width
        top:
          type: number
          minimum: 0
          maximum: 100
          description: Y position as percentage of media height
        width:
          type: number
          minimum: 0.1
          maximum: 100
          description: Width as percentage of media width
        height:
          type: number
          minimum: 0.1
          maximum: 100
          description: Height as percentage of media height
        scaleX:
          type: number
          minimum: 0.01
          maximum: 10
          description: Horizontal scaling factor
          default: 1
        scaleY:
          type: number
          minimum: 0.01
          maximum: 10
          description: Vertical scaling factor
          default: 1
        angle:
          type: number
          minimum: -360
          maximum: 360
          description: Rotation angle in degrees
          default: 0
        skewX:
          type: number
          minimum: -89
          maximum: 89
          description: X-axis skewing in degrees
          default: 0
        skewY:
          type: number
          minimum: -89
          maximum: 89
          description: Y-axis skewing in degrees
          default: 0
        flipX:
          type: boolean
          description: Horizontal flip
          default: false
        flipY:
          type: boolean
          description: Vertical flip
          default: false
        originX:
          type: string
          description: Transform origin X
          default: left
          enum:
            - left
            - center
            - right
        originY:
          type: string
          description: Transform origin Y
          default: top
          enum:
            - center
            - top
            - bottom
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Object opacity
          default: 1
        visible:
          type: boolean
          description: Object visibility
          default: true
        shadow:
          $ref: '#/components/schemas/SharedColorBlurOffsetX'
        strokeLineCap:
          type: string
          description: Line cap style
          default: butt
          enum:
            - butt
            - round
            - square
        strokeLineJoin:
          type: string
          description: Line join style
          default: miter
          enum:
            - miter
            - round
            - bevel
        strokeMiterLimit:
          type: number
          minimum: 1
          maximum: 20
          description: Miter limit for line joins
          default: 4
        strokeDashArray:
          type: array
          items:
            type: number
            minimum: 0.1
            maximum: 10
          maxItems: 10
          description: Dash pattern as multiples of stroke width
        fillRule:
          type: string
          description: Fill rule for complex shapes
          default: nonzero
          enum:
            - nonzero
            - evenodd
      required:
        - type
        - coordinates
      additionalProperties: false
      title: NestedDotAnnotationInput
      description: Nested dot annotation
    NestedShapeAnnotationInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - rectangle
            - circle
            - triangle
            - arrow
            - line
        coordinates:
          type: array
          items:
            type: object
            properties:
              x:
                type: number
                minimum: 0
                maximum: 100
                description: X coordinate as percentage (0-100)
              'y':
                type: number
                minimum: 0
                maximum: 100
                description: Y coordinate as percentage (0-100)
            required:
              - x
              - 'y'
            additionalProperties: false
            title: AnnotationCoordinatesInput
          minItems: 2
          maxItems: 50
          description: Array of coordinate points defining the shape
        strokeColor:
          type: string
          description: Stroke color as hex code
          default: '#000000'
        fillColor:
          type: string
          description: Fill color as hex code (optional)
          default: null
        strokeWidth:
          type: number
          minimum: 0
          maximum: 50
          description: Stroke width in pixels
          default: 2
        color:
          type: string
          description: Hex color code (e.g.,
          default: '#9b9b9b'
        frame:
          type: number
          description: Video frame number (for video annotations)
          default: 0
        left:
          type: number
          minimum: 0
          maximum: 100
          description: X position as percentage of media width
        top:
          type: number
          minimum: 0
          maximum: 100
          description: Y position as percentage of media height
        width:
          type: number
          minimum: 0.1
          maximum: 100
          description: Width as percentage of media width
        height:
          type: number
          minimum: 0.1
          maximum: 100
          description: Height as percentage of media height
        scaleX:
          type: number
          minimum: 0.01
          maximum: 10
          description: Horizontal scaling factor
          default: 1
        scaleY:
          type: number
          minimum: 0.01
          maximum: 10
          description: Vertical scaling factor
          default: 1
        angle:
          type: number
          minimum: -360
          maximum: 360
          description: Rotation angle in degrees
          default: 0
        skewX:
          type: number
          minimum: -89
          maximum: 89
          description: X-axis skewing in degrees
          default: 0
        skewY:
          type: number
          minimum: -89
          maximum: 89
          description: Y-axis skewing in degrees
          default: 0
        flipX:
          type: boolean
          description: Horizontal flip
          default: false
        flipY:
          type: boolean
          description: Vertical flip
          default: false
        originX:
          type: string
          description: Transform origin X
          default: left
          enum:
            - left
            - center
            - right
        originY:
          type: string
          description: Transform origin Y
          default: top
          enum:
            - center
            - top
            - bottom
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Object opacity
          default: 1
        visible:
          type: boolean
          description: Object visibility
          default: true
        shadow:
          $ref: '#/components/schemas/SharedColorBlurOffsetX'
        strokeLineCap:
          type: string
          description: Line cap style
          default: butt
          enum:
            - butt
            - round
            - square
        strokeLineJoin:
          type: string
          description: Line join style
          default: miter
          enum:
            - miter
            - round
            - bevel
        strokeMiterLimit:
          type: number
          minimum: 1
          maximum: 20
          description: Miter limit for line joins
          default: 4
        strokeDashArray:
          type: array
          items:
            type: number
            minimum: 0.1
            maximum: 10
          maxItems: 10
          description: Dash pattern as multiples of stroke width
        fillRule:
          type: string
          description: Fill rule for complex shapes
          default: nonzero
          enum:
            - nonzero
            - evenodd
      required:
        - type
        - coordinates
      additionalProperties: false
      title: NestedShapeAnnotationInput
      description: Nested shape annotation
    NestedTextAnnotationInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - text
        coordinates:
          type: object
          properties:
            x:
              type: number
              minimum: 0
              maximum: 100
              description: X coordinate as percentage (0-100)
            'y':
              type: number
              minimum: 0
              maximum: 100
              description: Y coordinate as percentage (0-100)
          required:
            - x
            - 'y'
          additionalProperties: false
          title: AnnotationCoordinatesInput
        content:
          type: string
          maxLength: 500
          description: Text content to display
        fontSize:
          type: number
          minimum: 8
          maximum: 72
          description: Font size in pixels
          default: 16
        fontFamily:
          type: string
          default: Arial
          enum:
            - Arial
            - Helvetica
            - Times New Roman
            - Courier New
            - Georgia
            - Verdana
        fontWeight:
          type: string
          default: normal
          enum:
            - normal
            - bold
        fontStyle:
          type: string
          default: normal
          enum:
            - normal
            - italic
        textColor:
          type: string
          description: Text color as hex code
          default: '#000000'
        color:
          type: string
          description: Hex color code (e.g.,
          default: '#9b9b9b'
        frame:
          type: number
          description: Video frame number (for video annotations)
          default: 0
        left:
          type: number
          minimum: 0
          maximum: 100
          description: X position as percentage of media width
        top:
          type: number
          minimum: 0
          maximum: 100
          description: Y position as percentage of media height
        width:
          type: number
          minimum: 0.1
          maximum: 100
          description: Width as percentage of media width
        height:
          type: number
          minimum: 0.1
          maximum: 100
          description: Height as percentage of media height
        scaleX:
          type: number
          minimum: 0.01
          maximum: 10
          description: Horizontal scaling factor
          default: 1
        scaleY:
          type: number
          minimum: 0.01
          maximum: 10
          description: Vertical scaling factor
          default: 1
        angle:
          type: number
          minimum: -360
          maximum: 360
          description: Rotation angle in degrees
          default: 0
        skewX:
          type: number
          minimum: -89
          maximum: 89
          description: X-axis skewing in degrees
          default: 0
        skewY:
          type: number
          minimum: -89
          maximum: 89
          description: Y-axis skewing in degrees
          default: 0
        flipX:
          type: boolean
          description: Horizontal flip
          default: false
        flipY:
          type: boolean
          description: Vertical flip
          default: false
        originX:
          type: string
          description: Transform origin X
          default: left
          enum:
            - left
            - center
            - right
        originY:
          type: string
          description: Transform origin Y
          default: top
          enum:
            - center
            - top
            - bottom
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Object opacity
          default: 1
        visible:
          type: boolean
          description: Object visibility
          default: true
        shadow:
          $ref: '#/components/schemas/SharedColorBlurOffsetX'
        strokeLineCap:
          type: string
          description: Line cap style
          default: butt
          enum:
            - butt
            - round
            - square
        strokeLineJoin:
          type: string
          description: Line join style
          default: miter
          enum:
            - miter
            - round
            - bevel
        strokeMiterLimit:
          type: number
          minimum: 1
          maximum: 20
          description: Miter limit for line joins
          default: 4
        strokeDashArray:
          type: array
          items:
            type: number
            minimum: 0.1
            maximum: 10
          maxItems: 10
          description: Dash pattern as multiples of stroke width
        fillRule:
          type: string
          description: Fill rule for complex shapes
          default: nonzero
          enum:
            - nonzero
            - evenodd
      required:
        - type
        - coordinates
        - content
      additionalProperties: false
      title: NestedTextAnnotationInput
      description: Nested text annotation
    NestedPathAnnotationInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - path
        pathData:
          type: string
          maxLength: 25000
          description: SVG path data string
        strokeColor:
          type: string
          description: Stroke color as hex code
          default: '#000000'
        strokeWidth:
          type: number
          minimum: 0
          maximum: 50
          description: Stroke width in pixels
          default: 2
        color:
          type: string
          description: Hex color code (e.g.,
          default: '#9b9b9b'
        frame:
          type: number
          description: Video frame number (for video annotations)
          default: 0
        left:
          type: number
          minimum: 0
          maximum: 100
          description: X position as percentage of media width
        top:
          type: number
          minimum: 0
          maximum: 100
          description: Y position as percentage of media height
        width:
          type: number
          minimum: 0.1
          maximum: 100
          description: Width as percentage of media width
        height:
          type: number
          minimum: 0.1
          maximum: 100
          description: Height as percentage of media height
        scaleX:
          type: number
          minimum: 0.01
          maximum: 10
          description: Horizontal scaling factor
          default: 1
        scaleY:
          type: number
          minimum: 0.01
          maximum: 10
          description: Vertical scaling factor
          default: 1
        angle:
          type: number
          minimum: -360
          maximum: 360
          description: Rotation angle in degrees
          default: 0
        skewX:
          type: number
          minimum: -89
          maximum: 89
          description: X-axis skewing in degrees
          default: 0
        skewY:
          type: number
          minimum: -89
          maximum: 89
          description: Y-axis skewing in degrees
          default: 0
        flipX:
          type: boolean
          description: Horizontal flip
          default: false
        flipY:
          type: boolean
          description: Vertical flip
          default: false
        originX:
          type: string
          description: Transform origin X
          default: left
          enum:
            - left
            - center
            - right
        originY:
          type: string
          description: Transform origin Y
          default: top
          enum:
            - center
            - top
            - bottom
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Object opacity
          default: 1
        visible:
          type: boolean
          description: Object visibility
          default: true
        shadow:
          $ref: '#/components/schemas/SharedColorBlurOffsetX'
        strokeLineCap:
          type: string
          description: Line cap style
          default: butt
          enum:
            - butt
            - round
            - square
        strokeLineJoin:
          type: string
          description: Line join style
          default: miter
          enum:
            - miter
            - round
            - bevel
        strokeMiterLimit:
          type: number
          minimum: 1
          maximum: 20
          description: Miter limit for line joins
          default: 4
        strokeDashArray:
          type: array
          items:
            type: number
            minimum: 0.1
            maximum: 10
          maxItems: 10
          description: Dash pattern as multiples of stroke width
        fillRule:
          type: string
          description: Fill rule for complex shapes
          default: nonzero
          enum:
            - nonzero
            - evenodd
      required:
        - type
        - pathData
      additionalProperties: false
      title: NestedPathAnnotationInput
      description: Nested path annotation
    NestedAnnotationInputList:
      type: array
      items:
        oneOf:
          - $ref: '#/components/schemas/NestedDotAnnotationInput'
          - $ref: '#/components/schemas/NestedShapeAnnotationInput'
          - $ref: '#/components/schemas/NestedTextAnnotationInput'
          - $ref: '#/components/schemas/NestedPathAnnotationInput'
        title: NestedAnnotationInput
      maxItems: 10
      description: >-
        Optional array of nested annotations (spatial annotations within this annotation's context). IMPORTANT - Nested
        annotations can ONLY be used when the parent annotation has a timestamp (timestamp, startTimestamp, or
        endTimestamp). Nested annotations inherit the parent's timestamp and cannot have their own timestamps or nested
        annotations.
    DotAnnotationInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - dot
        coordinates:
          type: object
          properties:
            x:
              type: number
              minimum: 0
              maximum: 100
              description: X coordinate as percentage (0-100)
            'y':
              type: number
              minimum: 0
              maximum: 100
              description: Y coordinate as percentage (0-100)
          required:
            - x
            - 'y'
          additionalProperties: false
          title: AnnotationCoordinatesInput
        radius:
          type: number
          minimum: 5
          maximum: 100
          default: 10
        color:
          type: string
          description: Hex color code (e.g.,
          default: '#9b9b9b'
        frame:
          type: number
          description: Video frame number (for video annotations)
          default: 0
        timestamp:
          type: number
          description: Legacy single timestamp in milliseconds (for backward compatibility)
        startTimestamp:
          type: number
          description: Range start timestamp or single point timestamp in milliseconds
        endTimestamp:
          type: number
          description: Range end timestamp in milliseconds (optional, creates range if provided)
        left:
          type: number
          minimum: 0
          maximum: 100
          description: X position as percentage of media width
        top:
          type: number
          minimum: 0
          maximum: 100
          description: Y position as percentage of media height
        width:
          type: number
          minimum: 0.1
          maximum: 100
          description: Width as percentage of media width
        height:
          type: number
          minimum: 0.1
          maximum: 100
          description: Height as percentage of media height
        scaleX:
          type: number
          minimum: 0.01
          maximum: 10
          description: Horizontal scaling factor
          default: 1
        scaleY:
          type: number
          minimum: 0.01
          maximum: 10
          description: Vertical scaling factor
          default: 1
        angle:
          type: number
          minimum: -360
          maximum: 360
          description: Rotation angle in degrees
          default: 0
        skewX:
          type: number
          minimum: -89
          maximum: 89
          description: X-axis skewing in degrees
          default: 0
        skewY:
          type: number
          minimum: -89
          maximum: 89
          description: Y-axis skewing in degrees
          default: 0
        flipX:
          type: boolean
          description: Horizontal flip
          default: false
        flipY:
          type: boolean
          description: Vertical flip
          default: false
        originX:
          type: string
          description: Transform origin X
          default: left
          enum:
            - left
            - center
            - right
        originY:
          type: string
          description: Transform origin Y
          default: top
          enum:
            - center
            - top
            - bottom
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Object opacity
          default: 1
        visible:
          type: boolean
          description: Object visibility
          default: true
        shadow:
          $ref: '#/components/schemas/SharedColorBlurOffsetX'
        strokeLineCap:
          type: string
          description: Line cap style
          default: butt
          enum:
            - butt
            - round
            - square
        strokeLineJoin:
          type: string
          description: Line join style
          default: miter
          enum:
            - miter
            - round
            - bevel
        strokeMiterLimit:
          type: number
          minimum: 1
          maximum: 20
          description: Miter limit for line joins
          default: 4
        strokeDashArray:
          type: array
          items:
            type: number
            minimum: 0.1
            maximum: 10
          maxItems: 10
          description: Dash pattern as multiples of stroke width
        fillRule:
          type: string
          description: Fill rule for complex shapes
          default: nonzero
          enum:
            - nonzero
            - evenodd
        nestedAnnotations:
          $ref: '#/components/schemas/NestedAnnotationInputList'
      required:
        - type
        - coordinates
      additionalProperties: false
      title: DotAnnotationInput
    FrameCommentAnnotationInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - frameComment
        color:
          type: string
          description: Hex color code (e.g.,
          default: '#9b9b9b'
        frame:
          type: number
          description: Video frame number (for video annotations)
          default: 0
        timestamp:
          type: number
          description: Legacy single timestamp in milliseconds (for backward compatibility)
        startTimestamp:
          type: number
          description: Range start timestamp or single point timestamp in milliseconds
        endTimestamp:
          type: number
          description: Range end timestamp in milliseconds (optional, creates range if provided)
        left:
          type: number
          minimum: 0
          maximum: 100
          description: X position as percentage of media width
        top:
          type: number
          minimum: 0
          maximum: 100
          description: Y position as percentage of media height
        width:
          type: number
          minimum: 0.1
          maximum: 100
          description: Width as percentage of media width
        height:
          type: number
          minimum: 0.1
          maximum: 100
          description: Height as percentage of media height
        scaleX:
          type: number
          minimum: 0.01
          maximum: 10
          description: Horizontal scaling factor
          default: 1
        scaleY:
          type: number
          minimum: 0.01
          maximum: 10
          description: Vertical scaling factor
          default: 1
        angle:
          type: number
          minimum: -360
          maximum: 360
          description: Rotation angle in degrees
          default: 0
        skewX:
          type: number
          minimum: -89
          maximum: 89
          description: X-axis skewing in degrees
          default: 0
        skewY:
          type: number
          minimum: -89
          maximum: 89
          description: Y-axis skewing in degrees
          default: 0
        flipX:
          type: boolean
          description: Horizontal flip
          default: false
        flipY:
          type: boolean
          description: Vertical flip
          default: false
        originX:
          type: string
          description: Transform origin X
          default: left
          enum:
            - left
            - center
            - right
        originY:
          type: string
          description: Transform origin Y
          default: top
          enum:
            - center
            - top
            - bottom
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Object opacity
          default: 1
        visible:
          type: boolean
          description: Object visibility
          default: true
        shadow:
          $ref: '#/components/schemas/SharedColorBlurOffsetX'
        strokeLineCap:
          type: string
          description: Line cap style
          default: butt
          enum:
            - butt
            - round
            - square
        strokeLineJoin:
          type: string
          description: Line join style
          default: miter
          enum:
            - miter
            - round
            - bevel
        strokeMiterLimit:
          type: number
          minimum: 1
          maximum: 20
          description: Miter limit for line joins
          default: 4
        strokeDashArray:
          type: array
          items:
            type: number
            minimum: 0.1
            maximum: 10
          maxItems: 10
          description: Dash pattern as multiples of stroke width
        fillRule:
          type: string
          description: Fill rule for complex shapes
          default: nonzero
          enum:
            - nonzero
            - evenodd
        nestedAnnotations:
          $ref: '#/components/schemas/NestedAnnotationInputList'
      required:
        - type
      additionalProperties: false
      title: FrameCommentAnnotationInput
    ShapeAnnotationInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - rectangle
            - circle
            - triangle
            - arrow
            - line
        coordinates:
          type: array
          items:
            type: object
            properties:
              x:
                type: number
                minimum: 0
                maximum: 100
                description: X coordinate as percentage (0-100)
              'y':
                type: number
                minimum: 0
                maximum: 100
                description: Y coordinate as percentage (0-100)
            required:
              - x
              - 'y'
            additionalProperties: false
            title: AnnotationCoordinatesInput
          minItems: 2
          maxItems: 50
          description: Array of coordinate points defining the shape
        strokeColor:
          type: string
          description: Stroke color as hex code
          default: '#000000'
        fillColor:
          type: string
          description: Fill color as hex code (optional)
          default: null
        strokeWidth:
          type: number
          minimum: 0
          maximum: 50
          description: Stroke width in pixels
          default: 2
        color:
          type: string
          description: Hex color code (e.g.,
          default: '#9b9b9b'
        frame:
          type: number
          description: Video frame number (for video annotations)
          default: 0
        timestamp:
          type: number
          description: Legacy single timestamp in milliseconds (for backward compatibility)
        startTimestamp:
          type: number
          description: Range start timestamp or single point timestamp in milliseconds
        endTimestamp:
          type: number
          description: Range end timestamp in milliseconds (optional, creates range if provided)
        left:
          type: number
          minimum: 0
          maximum: 100
          description: X position as percentage of media width
        top:
          type: number
          minimum: 0
          maximum: 100
          description: Y position as percentage of media height
        width:
          type: number
          minimum: 0.1
          maximum: 100
          description: Width as percentage of media width
        height:
          type: number
          minimum: 0.1
          maximum: 100
          description: Height as percentage of media height
        scaleX:
          type: number
          minimum: 0.01
          maximum: 10
          description: Horizontal scaling factor
          default: 1
        scaleY:
          type: number
          minimum: 0.01
          maximum: 10
          description: Vertical scaling factor
          default: 1
        angle:
          type: number
          minimum: -360
          maximum: 360
          description: Rotation angle in degrees
          default: 0
        skewX:
          type: number
          minimum: -89
          maximum: 89
          description: X-axis skewing in degrees
          default: 0
        skewY:
          type: number
          minimum: -89
          maximum: 89
          description: Y-axis skewing in degrees
          default: 0
        flipX:
          type: boolean
          description: Horizontal flip
          default: false
        flipY:
          type: boolean
          description: Vertical flip
          default: false
        originX:
          type: string
          description: Transform origin X
          default: left
          enum:
            - left
            - center
            - right
        originY:
          type: string
          description: Transform origin Y
          default: top
          enum:
            - center
            - top
            - bottom
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Object opacity
          default: 1
        visible:
          type: boolean
          description: Object visibility
          default: true
        shadow:
          $ref: '#/components/schemas/SharedColorBlurOffsetX'
        strokeLineCap:
          type: string
          description: Line cap style
          default: butt
          enum:
            - butt
            - round
            - square
        strokeLineJoin:
          type: string
          description: Line join style
          default: miter
          enum:
            - miter
            - round
            - bevel
        strokeMiterLimit:
          type: number
          minimum: 1
          maximum: 20
          description: Miter limit for line joins
          default: 4
        strokeDashArray:
          type: array
          items:
            type: number
            minimum: 0.1
            maximum: 10
          maxItems: 10
          description: Dash pattern as multiples of stroke width
        fillRule:
          type: string
          description: Fill rule for complex shapes
          default: nonzero
          enum:
            - nonzero
            - evenodd
        nestedAnnotations:
          $ref: '#/components/schemas/NestedAnnotationInputList'
      required:
        - type
        - coordinates
      additionalProperties: false
      title: ShapeAnnotationInput
    TextAnnotationInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - text
        coordinates:
          type: object
          properties:
            x:
              type: number
              minimum: 0
              maximum: 100
              description: X coordinate as percentage (0-100)
            'y':
              type: number
              minimum: 0
              maximum: 100
              description: Y coordinate as percentage (0-100)
          required:
            - x
            - 'y'
          additionalProperties: false
          title: AnnotationCoordinatesInput
        content:
          type: string
          maxLength: 500
          description: Text content to display
        fontSize:
          type: number
          minimum: 8
          maximum: 72
          description: Font size in pixels
          default: 16
        fontFamily:
          type: string
          default: Arial
          enum:
            - Arial
            - Helvetica
            - Times New Roman
            - Courier New
            - Georgia
            - Verdana
        fontWeight:
          type: string
          default: normal
          enum:
            - normal
            - bold
        fontStyle:
          type: string
          default: normal
          enum:
            - normal
            - italic
        textColor:
          type: string
          description: Text color as hex code
          default: '#000000'
        color:
          type: string
          description: Hex color code (e.g.,
          default: '#9b9b9b'
        frame:
          type: number
          description: Video frame number (for video annotations)
          default: 0
        timestamp:
          type: number
          description: Legacy single timestamp in milliseconds (for backward compatibility)
        startTimestamp:
          type: number
          description: Range start timestamp or single point timestamp in milliseconds
        endTimestamp:
          type: number
          description: Range end timestamp in milliseconds (optional, creates range if provided)
        left:
          type: number
          minimum: 0
          maximum: 100
          description: X position as percentage of media width
        top:
          type: number
          minimum: 0
          maximum: 100
          description: Y position as percentage of media height
        width:
          type: number
          minimum: 0.1
          maximum: 100
          description: Width as percentage of media width
        height:
          type: number
          minimum: 0.1
          maximum: 100
          description: Height as percentage of media height
        scaleX:
          type: number
          minimum: 0.01
          maximum: 10
          description: Horizontal scaling factor
          default: 1
        scaleY:
          type: number
          minimum: 0.01
          maximum: 10
          description: Vertical scaling factor
          default: 1
        angle:
          type: number
          minimum: -360
          maximum: 360
          description: Rotation angle in degrees
          default: 0
        skewX:
          type: number
          minimum: -89
          maximum: 89
          description: X-axis skewing in degrees
          default: 0
        skewY:
          type: number
          minimum: -89
          maximum: 89
          description: Y-axis skewing in degrees
          default: 0
        flipX:
          type: boolean
          description: Horizontal flip
          default: false
        flipY:
          type: boolean
          description: Vertical flip
          default: false
        originX:
          type: string
          description: Transform origin X
          default: left
          enum:
            - left
            - center
            - right
        originY:
          type: string
          description: Transform origin Y
          default: top
          enum:
            - center
            - top
            - bottom
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Object opacity
          default: 1
        visible:
          type: boolean
          description: Object visibility
          default: true
        shadow:
          $ref: '#/components/schemas/SharedColorBlurOffsetX'
        strokeLineCap:
          type: string
          description: Line cap style
          default: butt
          enum:
            - butt
            - round
            - square
        strokeLineJoin:
          type: string
          description: Line join style
          default: miter
          enum:
            - miter
            - round
            - bevel
        strokeMiterLimit:
          type: number
          minimum: 1
          maximum: 20
          description: Miter limit for line joins
          default: 4
        strokeDashArray:
          type: array
          items:
            type: number
            minimum: 0.1
            maximum: 10
          maxItems: 10
          description: Dash pattern as multiples of stroke width
        fillRule:
          type: string
          description: Fill rule for complex shapes
          default: nonzero
          enum:
            - nonzero
            - evenodd
        nestedAnnotations:
          $ref: '#/components/schemas/NestedAnnotationInputList'
      required:
        - type
        - coordinates
        - content
      additionalProperties: false
      title: TextAnnotationInput
    PathAnnotationInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - path
        pathData:
          type: string
          maxLength: 25000
          description: SVG path data string
        strokeColor:
          type: string
          description: Stroke color as hex code
          default: '#000000'
        strokeWidth:
          type: number
          minimum: 0
          maximum: 50
          description: Stroke width in pixels
          default: 2
        color:
          type: string
          description: Hex color code (e.g.,
          default: '#9b9b9b'
        frame:
          type: number
          description: Video frame number (for video annotations)
          default: 0
        timestamp:
          type: number
          description: Legacy single timestamp in milliseconds (for backward compatibility)
        startTimestamp:
          type: number
          description: Range start timestamp or single point timestamp in milliseconds
        endTimestamp:
          type: number
          description: Range end timestamp in milliseconds (optional, creates range if provided)
        left:
          type: number
          minimum: 0
          maximum: 100
          description: X position as percentage of media width
        top:
          type: number
          minimum: 0
          maximum: 100
          description: Y position as percentage of media height
        width:
          type: number
          minimum: 0.1
          maximum: 100
          description: Width as percentage of media width
        height:
          type: number
          minimum: 0.1
          maximum: 100
          description: Height as percentage of media height
        scaleX:
          type: number
          minimum: 0.01
          maximum: 10
          description: Horizontal scaling factor
          default: 1
        scaleY:
          type: number
          minimum: 0.01
          maximum: 10
          description: Vertical scaling factor
          default: 1
        angle:
          type: number
          minimum: -360
          maximum: 360
          description: Rotation angle in degrees
          default: 0
        skewX:
          type: number
          minimum: -89
          maximum: 89
          description: X-axis skewing in degrees
          default: 0
        skewY:
          type: number
          minimum: -89
          maximum: 89
          description: Y-axis skewing in degrees
          default: 0
        flipX:
          type: boolean
          description: Horizontal flip
          default: false
        flipY:
          type: boolean
          description: Vertical flip
          default: false
        originX:
          type: string
          description: Transform origin X
          default: left
          enum:
            - left
            - center
            - right
        originY:
          type: string
          description: Transform origin Y
          default: top
          enum:
            - center
            - top
            - bottom
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Object opacity
          default: 1
        visible:
          type: boolean
          description: Object visibility
          default: true
        shadow:
          $ref: '#/components/schemas/SharedColorBlurOffsetX'
        strokeLineCap:
          type: string
          description: Line cap style
          default: butt
          enum:
            - butt
            - round
            - square
        strokeLineJoin:
          type: string
          description: Line join style
          default: miter
          enum:
            - miter
            - round
            - bevel
        strokeMiterLimit:
          type: number
          minimum: 1
          maximum: 20
          description: Miter limit for line joins
          default: 4
        strokeDashArray:
          type: array
          items:
            type: number
            minimum: 0.1
            maximum: 10
          maxItems: 10
          description: Dash pattern as multiples of stroke width
        fillRule:
          type: string
          description: Fill rule for complex shapes
          default: nonzero
          enum:
            - nonzero
            - evenodd
        nestedAnnotations:
          $ref: '#/components/schemas/NestedAnnotationInputList'
      required:
        - type
        - pathData
      additionalProperties: false
      title: PathAnnotationInput
    Dot3dAnnotationInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - dot3d
        position:
          type: object
          properties:
            x:
              type: number
              minimum: -10000
              maximum: 10000
              description: X coordinate as percentage (0-100)
            'y':
              type: number
              minimum: -10000
              maximum: 10000
              description: Y coordinate as percentage (0-100)
            z:
              type: number
              minimum: -10000
              maximum: 10000
          required:
            - x
            - 'y'
            - z
          additionalProperties: false
        normal:
          type: object
          properties:
            x:
              type: number
              minimum: -1
              maximum: 1
              description: X coordinate as percentage (0-100)
            'y':
              type: number
              minimum: -1
              maximum: 1
              description: Y coordinate as percentage (0-100)
            z:
              type: number
              minimum: -1
              maximum: 1
          required:
            - x
            - 'y'
            - z
          additionalProperties: false
        scale:
          type: number
          minimum: 0.1
          maximum: 10
          default: 1
        color:
          type: string
          description: Hex color code (e.g.,
          default: '#9b9b9b'
        nestedAnnotations:
          $ref: '#/components/schemas/NestedAnnotationInputList'
      required:
        - type
        - position
        - normal
      additionalProperties: false
      title: Dot3dAnnotationInput
    AnnotationInputList:
      type: array
      items:
        oneOf:
          - $ref: '#/components/schemas/DotAnnotationInput'
          - $ref: '#/components/schemas/FrameCommentAnnotationInput'
          - $ref: '#/components/schemas/ShapeAnnotationInput'
          - $ref: '#/components/schemas/TextAnnotationInput'
          - $ref: '#/components/schemas/PathAnnotationInput'
          - $ref: '#/components/schemas/Dot3dAnnotationInput'
        title: AnnotationInput
      maxItems: 100
      description: >-
        Annotations attached to this message. `attachments`, `mentions`, `assetMentions` and `quotes` are rejected in
        public chats.
    SharedContentReplyToIdAnnotations:
      type: object
      properties:
        content:
          type: string
          minLength: 1
          maxLength: 10000
          description: The message content (HTML-sanitized server-side)
        replyToId:
          type: string
          format: uuid
          description: ID of the message being replied to
        annotations:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/DotAnnotationInput'
              - $ref: '#/components/schemas/FrameCommentAnnotationInput'
              - $ref: '#/components/schemas/ShapeAnnotationInput'
              - $ref: '#/components/schemas/TextAnnotationInput'
              - $ref: '#/components/schemas/PathAnnotationInput'
              - $ref: '#/components/schemas/Dot3dAnnotationInput'
            title: AnnotationInput
          maxItems: 100
          description: Array of annotations for images/videos
        guestId:
          type: string
          format: uuid
          description: |
            Client-generated identity for an anonymous commenter, persisted
            in the visitor's localStorage per token. Keeps two same-named
            guests distinct. Ignored when authenticated.
        guestName:
          type: string
          minLength: 1
          maxLength: 40
          description: |
            Display name for an anonymous commenter (HTML-sanitized). Required
            (and only used) when posting without authentication on an opted-in
            release.
        guestColor:
          type: string
          enum:
            - '#37474F'
            - '#FF5722'
            - '#2962FF'
            - '#33691E'
            - '#00796B'
            - '#455A64'
            - '#2979FF'
            - '#827717'
            - '#7986CB'
            - '#8E24AA'
            - '#9575CD'
            - '#BF360C'
            - '#01579B'
            - '#EF6C00'
            - '#AA00FF'
            - '#F44336'
            - '#7C4DFF'
            - '#E65100'
            - '#8D6E63'
            - '#283593'
            - '#607D8B'
            - '#009688'
            - '#FF5252'
            - '#03A9F4'
            - '#C2185B'
            - '#00ACC1'
            - '#E91E63'
            - '#5D4037'
            - '#78909C'
            - '#1E88E5'
            - '#D500F9'
            - '#7E57C2'
            - '#5C6BC0'
            - '#558B2F'
            - '#2E7D32'
            - '#F50057'
            - '#004D40'
            - '#0D47A1'
            - '#C51162'
            - '#D50000'
            - '#6200EA'
            - '#00BCD4'
            - '#0277BD'
          description: |
            Color the anonymous commenter selected — the visible
            differentiator between guests. Must be one of the platform's
            approved colors (`colors.approvedColors` from `GET /config`).
            Ignored when authenticated.
          example: '#2962FF'
      required:
        - content
      additionalProperties: false
  responses:
    UnknownError:
      description: An unknown error has occurred.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: unknownError
            code: 500
            message: An unknown error has occurred.
    Unauthorized:
      description: Authentication is required but was missing or failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: unauthorized
            code: 401
            message: Please authenticate
    Forbidden:
      description: The authenticated user does not have permission to perform this action.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: forbidden
            code: 403
            message: Forbidden
    CapabilityNotAvailable:
      description: |
        Returned by `requireCapability(...)`-gated routes when the
        workspace's plan does not include the capability or the
        per-capability opt-in setting (`aiAddOnEnabled`, `boardsEnabled`,
        `convosEnabled`) is off. Distinct from `Forbidden` because the
        caller's user-level permission is fine — what's missing is the
        workspace-level entitlement.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: capabilityNotAvailable
            code: 403
            message: This feature is not available on the workspace’s current plan or has not been enabled.
    NotFound:
      description: The requested resource could not be found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: notFound
            code: 404
            message: Not found
    UserNotFound:
      description: The specified user could not be found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: userNotFound
            code: 404
            message: User not found.
    WorkspaceNotFound:
      description: The specified workspace could not be found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: workspaceNotFound
            code: 404
            message: Workspace not found.
    ProjectNotFound:
      description: The specified project could not be found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: projectNotFound
            code: 404
            message: Project not found.
    InviteNotFound:
      description: The specified invitation could not be found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: inviteNotFound
            code: 404
            message: Invite not found.
    AssetNotFound:
      description: The specified asset could not be found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: assetNotFound
            code: 400
            message: Asset not found.
    FileNotFound:
      description: The specified file associated with an asset could not be found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: fileNotFound
            code: 400
            message: File not found.
    ChatNotFound:
      description: The specified chat could not be found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: chatNotFound
            code: 404
            message: Chat does not exist.
    BadRequest:
      description: The request could not be understood or was missing required parameters.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: badRequest
            code: 400
            message: Bad request.
    TaskNotFound:
      description: The specified task could not be found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: taskNotFound
            code: 404
            message: Task not found.
    SubmissionNotFound:
      description: The specified submission could not be found in this project.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: submissionNotFound
            code: 404
            message: Submission not found.
    PublicFileSystemNotFound:
      description: The specified public file system could not be found in this project.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            type: publicFileSystemNotFound
            code: 404
            message: Public file system not found
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
tags:
  - name: AccessActivity
    description: |
      Play, download and embed metrics for assets. `download` and
      `embed_resolved` events are recorded automatically by the API whenever
      a signed download URL is issued, embed files are resolved, or a file is
      downloaded from the Nurama web app. `play_started` and `play_completed`
      events are reported by the player through the POST endpoints below.
      Reads return aggregated rollups suitable for dashboards.

      Counts are aggregated per day for each combination of `(assetId,
      eventType, visibility, referrerHost, country, userAgentClass)`, so all
      plays of one asset on one day from one host contribute to a single
      bucket. The `visibility` dimension is the audience (`creator` |
      `reviewer` | `public`), matching the visibility model used elsewhere.
  - name: AI
    description: |
      AI features exposed under `/v1/ai/*`. Every AI call is metered against the
      calling workspace's Nurama Credit balance, except Nurama Support chat,
      which is free.

      Surfaces:

        - **Polish** (`POST /ai/polish`) — rewrite a draft message in a chosen tone.
        - **Compose** (`POST /ai/compose`) — multi-turn "Compose with Nu" drafting
          conversation for a chat; returns commentary plus an optional proposed
          message.
        - **Task generation** (`POST /ai/task-generation`) — turn a chat message
          (plus surrounding context) into one or more board-task drafts. Requires
          both the AI and the Boards capabilities on the workspace.
        - **AI chat** (`/ai/chat/*`) — multi-turn private conversation with the
          Nurama assistant, scoped to a workspace, a project, or a personal
          (social) chat. The assistant can call tools on the caller's behalf.
        - **Image revision** (`/ai/revisions*`) — prompt-guided image edits, staged
          as temporary Scratch uploads until promoted to an asset.
        - **Support chat** (`/ai/support-chat/*`) — free Nurama Support assistant,
          outside any workspace.
        - **Balance / usage** (`/ai/balance`, `/ai/usage`) — also available at
          `/credits/balance` and `/credits/usage`.

      **Credits.** A workspace's balance is the sum of its monthly plan grant
      (`planBalance`, which does not carry over) and any purchased top-ups
      (`purchasedBalance`, which accumulate). Each billable call is charged its
      actual cost once it completes; the response reports `billedCredits` and
      `balanceAfter`. If the AI provider call fails, nothing is charged.

      **What every billable AI endpoint checks**, in order. Each check has its
      own error `type`:

        1. The caller holds the endpoint's permission on the workspace
           (`forbidden`, 403).
        2. The workspace has an active subscription (`subscriptionNotActive`, 400).
        3. The workspace's plan includes the `ai` capability and the AI add-on is
           switched on for the workspace (`capabilityNotAvailable`, 403, with
           `errorData: { capability, workspaceId }`).
        4. The feature is enabled in the effective AI settings — a project's
           settings override the workspace's, key by key. `allowAiFeatures` and
           the per-feature flag (`aiPolishEnabled`, `aiComposeEnabled`,
           `aiChatEnabled`, `aiTaskGenerationEnabled`, `aiImageRevisionEnabled`)
           must both be on, and reviewers additionally need the matching
           `*AllowReviewer` flag (`aiFeatureNotEnabled`, 403).
        5. The workspace balance covers the worst-case cost of the call and the
           caller is under their per-user daily AI spend budget
           (`creditBalanceInsufficient` or `creditOrderExceedsUserDailySpendLimit`,
           400, with `errorData` describing the shortfall).

      **Live updates.** AI chat activity is delivered on the caller's
      `/user/{userId}` WebSocket channel as `notification` events whose `type`
      is `aiChatTopicCreate`, `aiChatTopicUpdate` or `aiChatMessageCreate`.
      Assistant replies carry the Nurama AI system user as the initiator;
      messages the user sends carry their own id.
  - name: Assets
    description: Manage assets and files.
  - name: Boards
    description: Kanban boards for organizing project tasks into columns.
  - name: Board Columns
    description: Manage columns within a kanban board.
  - name: Board Tasks
    description: Create, move, and manage tasks on a kanban board.
  - name: Task Details
    description: Update task details (subject, description, assignee) outside of board context.
  - name: Task Links
    description: Link and unlink tasks with relationship types (related, blocks, blockedBy, duplicate).
  - name: Board Following
    description: Follow and unfollow boards to receive notifications about new tasks.
  - name: Bots
    description: >-
      Bot user management for programmatic API access. A bot is a workspace-scoped user (accountType 'bot') that
      authenticates with an API key instead of a JWT. Limited to 2 free bots per workspace. Requires the canManageBots
      permission.
  - name: Chats
    description: Chat and ChatMessage routes.
  - name: Config
    description: >-
      Public platform configuration that clients read at startup — validation limits, roles and permissions, supported
      file types and feature flags.
  - name: Convos
    description: >
      Video and audio conversation (convo) routes for real-time communication using Daily.co integration. Every
      `/convos` route is behind the server-side convos feature flag and responds with 403 `featureDisabled` when it is
      off.
  - name: Folders
    description: Folder management and retrieval
  - name: Invites
    description: >-
      API for invite management. Note - InviteeId and Invitee object will only be added to response if the invite has
      already been accepted.
  - name: Link Preview
    description: >-
      Fetch rich link preview metadata (Open Graph / meta tags) for URLs. Previews are signed with HMAC-SHA256 to
      prevent spoofing when submitted with chat messages.
  - name: Membership
    description: >-
      Membership management and retrieval. New membership can only be created through an invite. Once a record exists
      new roles may be added.
  - name: Notifications
    description: |
      Retrieve notifications and notification last seen records.

      Channels are strings of the form `[resourceType]/[resourceId]` or
      `[resourceType]/[resourceId]/[visibility]` (a leading `/` is tolerated).
      `types` values are notification type names — the same `type` values
      carried by WebSocket `notification` events (for example `chatMessageCreate`
      or `taskCreate`); every type is documented in the WebSocket event
      reference. `POST /notifications`, `/new`, `/count` and `/count/bulk`
      require the `canGetNotificationChannels` permission: the caller must be
      allowed to read every channel named in the body, otherwise `403`.
  - name: Projects
    description: Project management and retrieval
  - name: Public
    description: Public file system access (no authentication required)
  - name: PublicAssetLinks
    description: Manage temporary, publicly shareable download links for individual assets.
  - name: Scratch
    description: |
      Scratch objects are temporary uploads that live outside a project's
      asset tree until they are either promoted to a real Asset or expire.
      Each scratch object has an explicit lifecycle:
      `pendingUpload` → `active` → `promoted` or `expired`.

      **The upload flow mirrors assets.** The operation that starts a job
      (for example `POST /v1/ai/revisions`) returns a
      `{ scratchId, uploadId, key, urls[] }` bundle in the same shape
      `POST /v1/assets` returns for asset uploads. Upload each part
      directly to its signed URL, then call
      `POST /v1/scratch/{scratchId}/complete-upload` with the collected
      `{ uploadId, parts }` — the same body `POST /v1/assets/complete-upload`
      accepts.

      **There is no endpoint for creating scratch objects or requesting
      upload URLs directly.** Upload URLs are issued by the operation that
      owns the job, and discarding a scratch object is also handled by that
      operation so the audit and billing trail stays tied to a real job.
      Promotion to a real Asset is exposed via
      `POST /v1/scratch/{scratchId}/promote`.

      Scratch bytes count toward the workspace's storage quota. When a
      scratch object expires or is discarded its bytes are released; when
      it is promoted the bytes are accounted for on the resulting Asset
      instead, so nothing is counted twice.
  - name: Settings
    description: Resource-specific user settings management
  - name: ShortLinks
    description: Short link generation and resolution for shareable deep links.
  - name: Storage
    description: Retrieve resource storage data.
  - name: Task Relations
    description: >
      "Related To" links between board tasks and chats / chat messages. Every route requires the workspace `boards`
      capability (403 `capabilityNotAvailable` otherwise) and an active workspace subscription.
  - name: Tasks
    description: Retrieve and update tasks assigned to logged in user.
  - name: Users
    description: >
      User management and retrieval


      Delegated credentials (personal access tokens, bot keys and OAuth access tokens) are rejected on PATCH /users and
      DELETE /users (GET /users stays available to delegated credentials) with 403 `delegatedTokenForbidden`; a
      first-party user session is required.
  - name: Version
    description: Version and other deployment information.
  - name: Webhook Subscriptions
    description: >-
      Outbound webhook subscriptions. A workspace admin registers an HTTPS URL plus the event names it wants; Nurama
      POSTs a signed JSON envelope to that URL whenever a matching notification is written. All endpoints require
      `canManageWebhooks` on the workspace and either a Nurama app session or a personal access token carrying the
      `webhooks:manage` scope. Bot API keys and MCP connections cannot manage subscriptions. A workspace may hold at
      most 25 subscriptions that are not `failedOut`.


      **Signing.** Every subscription has an HMAC signing secret. The plaintext is returned exactly twice in its
      lifetime — on create and on rotate-secret — and is otherwise unrecoverable. Every other response is sanitised (the
      encrypted secret and its key version are never returned).


      **Event catalogue:** `task.created`, `task.updated` (covers move / assign / subject / description edits),
      `task.deleted`, `chat.message.created`, `asset.published` (only the transition to `active`), and `webhook.test`
      (synthetic; only fired by the `/test` endpoint and delivered to the targeted subscription regardless of its
      `events` list). Event names are not versioned; the wire contract is additive-only.


      **Lifecycle.** `active` → delivering; `paused` → manually paused by an admin; `failedOut` → automatically paused
      after 50 consecutive deliveries exhausted their retries (`failedOutAt` / `failedOutReason` set). Admins re-enable
      by PATCHing `status: active`, which clears the failed-out audit fields. An optional `expiresAt` stops fan-out once
      in the past.
  - name: Workspaces
    description: Workspaces management and retrieval
