openapi: 3.1.0
info:
  title: Kavanah API
  version: 1.0.0
  description: |
    RESTful API for the Kavanah project management platform.
    All endpoints (except auth) require a valid session cookie.
  contact:
    name: Kavanah Support
servers:
  - url: http://localhost:3000
    description: Local development
paths:
  /api/apps:
    get:
      summary: List the workspace's installed community apps
      tags:
        - apps
      x-required-permission: owner/admin
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/apps/installs/{id}:
    delete:
      summary: Uninstall a community app from the workspace
      tags:
        - apps
      x-required-permission: owner/admin
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
    patch:
      summary: Reconfigure an installed app (enable, agent access, embedded UI, scopes)
      tags:
        - apps
      x-required-permission: owner/admin
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled:
                  type: boolean
                agentAccess:
                  type: boolean
                uiAccess:
                  type: boolean
                grantedScopes:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/apps/installs/{id}/keys:
    get:
      summary: List an installed app's API keys (metadata only)
      tags:
        - apps
      x-required-permission: owner/admin
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
    post:
      summary: Mint a per-app API key for an installed app (returned once)
      tags:
        - apps
      x-required-permission: owner/admin
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  default: App key
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/apps/installs/{id}/keys/{keyId}:
    delete:
      summary: Revoke a single per-app API key
      tags:
        - apps
      x-required-permission: owner/admin
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: keyId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/apps/installs/{id}/secret:
    post:
      summary: Mint or rotate an install's webhook signing secret (returned once)
      tags:
        - apps
      x-required-permission: owner/admin
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/apps/surfaces/{slug}/{surfaceId}:
    get:
      summary: Resolve an installed app's UI surface and mint its embed token
      tags:
        - apps
      x-required-permission: authenticated
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: surfaceId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/apps/{slug}/install:
    post:
      summary: Install a community app for the workspace with granted scopes
      tags:
        - apps
      x-required-permission: owner/admin
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                grantedScopes:
                  type: array
                  items:
                    type: string
                agentAccess:
                  type: boolean
      responses:
        "201":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/developer/apps:
    get:
      summary: List community apps this workspace owns (developer console)
      tags:
        - developer
      x-required-permission: owner/admin
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
    post:
      summary: Create a new private community app from a manifest
      tags:
        - developer
      x-required-permission: owner/admin
      responses:
        "201":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/developer/apps/draft-manifest:
    post:
      summary: Draft an app manifest from a plain-English description (AI)
      tags:
        - developer
      x-required-permission: owner/admin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                description:
                  type: string
              required:
                - description
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/developer/apps/{slug}:
    delete:
      summary: Delete a private app the workspace owns
      tags:
        - developer
      x-required-permission: owner/admin
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
    get:
      summary: Get authoring detail for one owned app (manifest + versions)
      tags:
        - developer
      x-required-permission: owner/admin
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/developer/apps/{slug}/reachability:
    post:
      summary: Check whether an owned app's tool endpoint is reachable
      tags:
        - developer
      x-required-permission: owner/admin
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/developer/apps/{slug}/submit:
    post:
      summary: Submit an app version for platform review (public listing)
      tags:
        - developer
      x-required-permission: owner/admin
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                versionId:
                  type: string
                  minLength: 1
              required:
                - versionId
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/developer/apps/{slug}/test-tool:
    post:
      summary: Send a signed test call to an owned app's tool endpoint
      tags:
        - developer
      x-required-permission: owner/admin
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/developer/apps/{slug}/versions:
    post:
      summary: Publish a new version of an owned app from a manifest
      tags:
        - developer
      x-required-permission: owner/admin
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "201":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/developer/apps/{slug}/versions/{id}:
    delete:
      summary: Delete a draft or rejected version of an owned app
      tags:
        - developer
      x-required-permission: owner/admin
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/developer/apps/{slug}/versions/{id}/rollback:
    post:
      summary: Set an older version of an owned app as the current version
      tags:
        - developer
      x-required-permission: owner/admin
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/feature-requests:
    get:
      summary: List the public feature-request board plus the caller's own submissions
      description: "Optional query params: status (active|all), category, search, limit (max 200)."
      tags:
        - Feature requests
      x-required-permission: authenticated
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
    post:
      summary: Submit a feature request
      tags:
        - Feature requests
      x-required-permission: authenticated
      responses:
        "201":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/marketplace:
    get:
      summary: List community apps visible to the workspace, with install state
      tags:
        - apps
      x-required-permission: authenticated
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/marketplace/{slug}:
    get:
      summary: Get one community app's detail and install state
      tags:
        - apps
      x-required-permission: authenticated
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
      security:
        - sessionCookie: []
        - apiKeyBearer: []
  /api/auth/signup:
    post:
      tags:
        - Auth
      summary: Create a new account
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - firstName
                - lastName
                - email
                - password
              properties:
                firstName:
                  type: string
                lastName:
                  type: string
                email:
                  type: string
                  format: email
                password:
                  type: string
                  minLength: 8
      responses:
        "200":
          description: Account created and signed in
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  signedIn:
                    type: boolean
                  redirectPath:
                    type: string
                  user:
                    type: object
                    properties:
                      email:
                        type: string
        "400":
          description: Validation error or duplicate email
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/auth/login:
    post:
      tags:
        - Auth
      summary: Sign in with email and password
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - password
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                nextPath:
                  type: string
      responses:
        "200":
          description: Signed in successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  signedIn:
                    type: boolean
                  workspaceReady:
                    type: boolean
                  redirectPath:
                    type: string
                  user:
                    type: object
                    properties:
                      email:
                        type: string
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/auth/logout:
    post:
      tags:
        - Auth
      summary: Sign out and clear session
      responses:
        "200":
          description: Signed out
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
  /api/auth/forgot-password:
    post:
      tags:
        - Auth
      summary: Request a password reset link
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Reset link sent (or silently ignored if email not found)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  message:
                    type: string
  /api/auth/reset-password:
    post:
      tags:
        - Auth
      summary: Reset password using a token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - password
                - token
              properties:
                password:
                  type: string
                  minLength: 8
                token:
                  type: string
      responses:
        "200":
          description: Password updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  redirectPath:
                    type: string
  /api/tasks:
    get:
      tags:
        - Tasks
      summary: List tasks for a project
      x-app-scope: tasks:read
      parameters:
        - name: projectRef
          in: query
          required: true
          schema:
            type: string
        - name: includeArchived
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: |
            Include archived tasks in the result (rows carry `archived` and `archived_at`). Default false — archived tasks are hidden.
      responses:
        "200":
          description: Tasks list
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  tasks:
                    type: array
                    items:
                      $ref: "#/components/schemas/Task"
    post:
      tags:
        - Tasks
      summary: Create a new task
      x-app-scope: tasks:write
      description: |
        Input keys are accepted in camelCase or snake_case (`dueDate` / `due_date`); camelCase wins when both are present. Responses are snake_case. Retries can pass an `Idempotency-Key` header: a replay within 5 minutes returns the original task instead of creating a duplicate (replays carry `X-Idempotent-Replay: 1`). Note `kvn_complete` is a generated column — returned by GET, never writable.
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 255
          description: |
            Opaque client token (charset `A-Za-z0-9._:-+/=`). Repeated creates with the same key within 5 minutes return the first result instead of inserting again.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - projectRef
                - title
              properties:
                projectRef:
                  type: string
                additionalProjectRefs:
                  type: array
                  items:
                    type: string
                  description: Secondary projects the task also appears in.
                title:
                  type: string
                description:
                  type: string
                statusId:
                  type: string
                  default: open
                tag:
                  type: string
                priority:
                  type: string
                  enum:
                    - Normal
                    - Medium
                    - High
                    - Done
                  default: Normal
                  description: Matched case-insensitively; stored in canonical casing.
                stage:
                  type: string
                  enum:
                    - fire
                    - green
                    - yellow
                    - investigate
                    - plan
                    - implement
                  nullable: true
                assignedTo:
                  nullable: true
                  oneOf:
                    - type: string
                    - type: array
                      items:
                        type: string
                dueDate:
                  type: string
                  format: date
                  nullable: true
                startDate:
                  type: string
                  format: date
                  nullable: true
                  description: Scheduled start (Timeline/Gantt bar).
                isMilestone:
                  type: boolean
                  description: Pins the task as a milestone on the roadmap.
                reminderDate:
                  type: string
                  nullable: true
                createdBy:
                  type: string
                parentTaskId:
                  type: integer
                  nullable: true
                  description: Link to a top-level parent task (first-class sub-task).
                departmentId:
                  type: string
                  nullable: true
                  description: UUID of the owning department (validated against the workspace).
                knowHow:
                  type: string
                  description: KVN K axis — method, verification, escalation.
                vision:
                  type: string
                  description: KVN V axis — the end-state.
                negation:
                  type: string
                  description: KVN N axis — what would make a working answer still wrong.
                visionRestated:
                  type: string
                  description: Delegate's V restatement (handoff loop).
                negationSurfaced:
                  type: string
                  description: Delegate's surfaced objection (handoff loop).
                subtasks:
                  type: array
                  items:
                    type: object
                notes:
                  type: array
                  items:
                    type: string
                links:
                  type: array
                  items:
                    type: object
                comments:
                  type: array
                  items:
                    type: object
                fileIds:
                  type: array
                  items:
                    type: string
                sortOrder:
                  type: integer
      responses:
        "201":
          description: Task created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  task:
                    $ref: "#/components/schemas/Task"
  /api/tasks/{id}:
    get:
      tags:
        - Tasks
      summary: Get a single task by id
      x-app-scope: tasks:read
      description: |
        Returns 404 (never 403) when the task exists but lives in a project the caller cannot see — a 403 would confirm the id exists.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: The task
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  task:
                    $ref: "#/components/schemas/Task"
        "404":
          description: Not found, or not visible to the caller
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      tags:
        - Tasks
      summary: Update a task
      x-app-scope: tasks:write
      description: |
        Input keys are accepted in camelCase or snake_case (`dueDate` / `due_date`); camelCase wins when both are present. Responses are snake_case. Omitting a field leaves it unchanged; where noted, `null` clears it. `kvn_complete` is a generated column and is never writable.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                description:
                  type: string
                statusId:
                  type: string
                tag:
                  type: string
                priority:
                  type: string
                  enum:
                    - Normal
                    - Medium
                    - High
                    - Done
                  description: Matched case-insensitively; stored in canonical casing.
                stage:
                  type: string
                  enum:
                    - fire
                    - green
                    - yellow
                    - investigate
                    - plan
                    - implement
                  nullable: true
                  description: "`null` clears the stage."
                emoji:
                  type: string
                  maxLength: 16
                  nullable: true
                  description: Single-emoji label; `null` or empty clears it.
                assignedTo:
                  oneOf:
                    - type: string
                    - type: array
                      items:
                        type: string
                dueDate:
                  type: string
                  format: date
                  nullable: true
                startDate:
                  type: string
                  format: date
                  nullable: true
                  description: Scheduled start (Timeline/Gantt bar).
                isMilestone:
                  type: boolean
                reminderDate:
                  type: string
                  nullable: true
                createdBy:
                  type: string
                sortOrder:
                  type: integer
                projectRefs:
                  type: array
                  items:
                    type: string
                  description: |
                    Replace the full set of projects this task belongs to. The primary project (or `primaryProjectRef`) must be included.
                primaryProjectRef:
                  type: string
                parentTaskId:
                  type: integer
                  nullable: true
                  description: Re-parent this task; `null` detaches it.
                departmentId:
                  type: string
                  nullable: true
                  description: Owning department UUID; `null` clears it.
                location:
                  type: object
                  nullable: true
                  description: Map pin; `null` clears it.
                  properties:
                    lat:
                      type: number
                      minimum: -90
                      maximum: 90
                    lng:
                      type: number
                      minimum: -180
                      maximum: 180
                    address:
                      type: string
                      maxLength: 500
                expectedVersion:
                  type: integer
                  description: Optimistic-concurrency guard; mismatch returns 409.
                knowHow:
                  type: string
                vision:
                  type: string
                negation:
                  type: string
                visionRestated:
                  type: string
                negationSurfaced:
                  type: string
                subtasks:
                  type: array
                  items:
                    type: object
                notes:
                  type: array
                  items:
                    type: string
                links:
                  type: array
                  items:
                    type: object
                comments:
                  type: array
                  items:
                    type: object
                fileIds:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Task updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  task:
                    $ref: "#/components/schemas/Task"
        "404":
          description: Task not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      tags:
        - Tasks
      summary: Delete a task
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Task deleted
        "404":
          description: Task not found
  /api/tasks/{id}/archive:
    post:
      tags:
        - Tasks
      summary: Archive or restore a task in place
      description: |
        Non-destructive hide: an archived task keeps its id, comments, and history but is excluded from task list queries unless `includeArchived=true` is passed to GET /api/tasks. Send `{"archived": false}` to restore. Requires the tasks/edit permission (unlike delete, nothing is lost).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                archived:
                  type: boolean
                  default: true
                  description: true archives the task, false restores it.
      responses:
        "200":
          description: Archive state updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  task:
                    $ref: "#/components/schemas/Task"
        "404":
          description: Task not found
  /api/tasks/link-previews:
    get:
      tags:
        - Tasks
      summary: Task cards for links posted in chat
      description: |
        Resolves task deep links into the cards the messenger renders inline. Filtered by the CALLER's own task visibility (project visibility plus the per-task ACL), so a task the caller cannot see is simply absent from the response and its link stays a plain URL. Ids that do not parse are dropped rather than failing the batch, because they come from parsing arbitrary message text. At most 60 ids per call.
      parameters:
        - name: ids
          in: query
          required: true
          description: Comma-separated task ids.
          schema:
            type: string
            example: 241,99
      responses:
        "200":
          description: Previews for the visible subset of the requested ids
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  previews:
                    type: array
                    items:
                      type: object
                      properties:
                        taskId:
                          type: integer
                        href:
                          type: string
                        title:
                          type: string
                        status:
                          type: string
                        priority:
                          type: string
                        emoji:
                          type: string
                          nullable: true
                        assignee:
                          type: string
                          nullable: true
                        dueDate:
                          type: string
                          nullable: true
                          format: date
                        projectRef:
                          type: string
                          nullable: true
                        projectName:
                          type: string
                          nullable: true
  /api/tasks/{id}/share-to-chat:
    post:
      tags:
        - Tasks
      summary: Send a task to teammates as chat messages
      description: |
        Posts the task's canonical link as an ordinary chat DM to each recipient, so push, unread badges and mute rules all apply. The message carries only the link — the recipient's own access decides whether it renders as a card — so this does not grant access and deliberately does not check the recipients' access. The SENDER must be able to view the task. Per-recipient failures are reported rather than aborting the rest.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - recipientIds
              properties:
                recipientIds:
                  type: array
                  maxItems: 20
                  items:
                    type: string
                    format: uuid
                note:
                  type: string
                  maxLength: 2000
      responses:
        "200":
          description: Sent to at least one recipient
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  sentTo:
                    type: array
                    items:
                      type: string
                  failedTo:
                    type: array
                    items:
                      type: string
        "400":
          description: No recipients, or too many
        "404":
          description: Task not found, or not visible to the caller
        "502":
          description: Could not send to any selected recipient
  /api/tasks/reorder:
    post:
      tags:
        - Tasks
      summary: Reorder tasks (bulk status + sort update)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tasks
              properties:
                tasks:
                  type: array
                  items:
                    type: object
                    required:
                      - id
                      - statusId
                      - sortOrder
                      - projectRef
                    properties:
                      id:
                        type: integer
                      statusId:
                        type: string
                      sortOrder:
                        type: integer
                      projectRef:
                        type: string
      responses:
        "200":
          description: Tasks reordered
  /api/tasks/import:
    post:
      tags:
        - Tasks
      summary: Import tasks from CSV
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - projectRef
                - csv
              properties:
                projectRef:
                  type: string
                csv:
                  type: string
      responses:
        "200":
          description: Tasks imported
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  importedCount:
                    type: integer
                  skippedRowCount:
                    type: integer
  /api/tasks/export:
    get:
      tags:
        - Tasks
      summary: Export a project's tasks as CSV, or the full board as JSON
      parameters:
        - name: projectRef
          in: query
          required: true
          schema:
            type: string
        - name: projectName
          in: query
          schema:
            type: string
        - name: format
          in: query
          description: CSV variant (standard/jira/asana) or `json` for a structured board export (project + ordered columns, each carrying its tasks).
          schema:
            type: string
            enum:
              - standard
              - jira
              - asana
              - json
      responses:
        "200":
          description: CSV file (CSV formats) or JSON board export (`format=json`)
          content:
            text/csv:
              schema:
                type: string
            application/json:
              schema:
                type: object
  /api/clients:
    get:
      tags:
        - Clients
      summary: List all clients
      x-app-scope: clients:read
      responses:
        "200":
          description: Clients list with owner options
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  clients:
                    type: array
                    items:
                      $ref: "#/components/schemas/Client"
                  ownerOptions:
                    type: array
                    items:
                      type: object
    post:
      tags:
        - Clients
      summary: Create a new client
      x-app-scope: clients:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - company
                - contactName
                - email
              properties:
                company:
                  type: string
                contactName:
                  type: string
                email:
                  type: string
                  format: email
                contactRole:
                  type: string
                location:
                  type: string
                website:
                  type: string
                ownerId:
                  type: string
                ownerName:
                  type: string
                stage:
                  type: string
                health:
                  type: string
                invoiceStatus:
                  type: string
                arr:
                  oneOf:
                    - type: string
                    - type: number
                priority:
                  type: boolean
                archived:
                  type: boolean
      responses:
        "201":
          description: Client created
  /api/clients/{id}:
    get:
      tags:
        - Clients
      summary: Get a client by ID
      x-app-scope: clients:read
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Client details
        "404":
          description: Client not found
    patch:
      tags:
        - Clients
      summary: Update a client
      x-app-scope: clients:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Client updated
        "404":
          description: Client not found
    delete:
      tags:
        - Clients
      summary: Delete a client
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Client deleted
        "404":
          description: Client not found
  /api/clients/{id}/portal-access:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Clients
      summary: List portal members for a client
      responses:
        "200":
          description: Portal members list
    post:
      tags:
        - Clients
      summary: Grant or update portal access for a client employee
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - memberId
              properties:
                email:
                  type: string
                memberId:
                  type: string
                name:
                  type: string
                role:
                  type: string
                portalEnabled:
                  type: boolean
                canMessage:
                  type: boolean
                confirmReinvite:
                  type: boolean
      responses:
        "200":
          description: Portal member created or updated
        "409":
          description: Member was previously revoked; reinvite confirmation required
    delete:
      tags:
        - Clients
      summary: Revoke portal access for a client employee
      parameters:
        - name: memberId
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Portal access revoked
        "404":
          description: Portal member not found
  /api/clients/{id}/portal-access/{memberId}:
    patch:
      tags:
        - Clients
      summary: Update a portal member's access
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: memberId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                portalEnabled:
                  type: boolean
                canMessage:
                  type: boolean
      responses:
        "200":
          description: Portal member updated
  /api/projects:
    get:
      tags:
        - Projects
      summary: Get workspace project bundle
      x-app-scope: projects:read
      responses:
        "200":
          description: Project workspace bundle
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  bundle:
                    $ref: "#/components/schemas/ProjectWorkspaceBundle"
                  mode:
                    type: string
    post:
      tags:
        - Projects
      summary: Create a project
      x-app-scope: projects:write
      description: Creates a single project. Only `name` is required — the 3-step UI wizard asks for a project type, but that is a UI affordance, not a data requirement. Honours `Idempotency-Key`.
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
          description: Opaque key; a retry replays the original response.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: Project name. The only required field.
                projectType:
                  type: string
                description:
                  type: string
                clientId:
                  type:
                    - string
                    - "null"
                startDate:
                  type:
                    - string
                    - "null"
                  description: Calendar date (YYYY-MM-DD).
                deadline:
                  type:
                    - string
                    - "null"
                  description: Calendar date (YYYY-MM-DD).
                visibility:
                  type: string
                  enum:
                    - public
                    - private
                members:
                  type: array
                  items:
                    type: object
                    properties:
                      name:
                        type: string
                      email:
                        type: string
                      team:
                        type: string
      responses:
        "201":
          description: Project created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  project:
                    type: object
                  bundle:
                    $ref: "#/components/schemas/ProjectWorkspaceBundle"
        "400":
          description: Invalid body
        "402":
          description: Plan project limit reached
    patch:
      tags:
        - Projects
      summary: Update workspace project bundle
      description: "Merges the submitted bundle onto the workspace's stored bundle. A PATCH never removes a project: one missing from `projects` (or sent with a non-numeric id or empty name) is KEPT. Deleting a project is `DELETE /api/projects/{id}`, which snapshots it to trash and cascades into its tasks, time entries, discussions, files and budget rows. `workspaceId` names the workspace the bundle was loaded from and `bundleVersion` the row version it was built on (every bundle the API returns carries both, as `workspaceId` and `version`): the save is refused with 409 when either is not what the server holds, and with 400 when a browser session omits either. API-key callers may omit them; when they echo, they are checked."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/ProjectWorkspaceBundle"
                - type: object
                  properties:
                    workspaceId:
                      type: string
                      description: The workspace this bundle was loaded from, as returned by GET /api/projects. Required for browser sessions.
                    bundleVersion:
                      type: string
                      description: The `version` of the bundle this save was built on, as returned by GET /api/projects (the row's `updated_at`). Required for browser sessions.
      responses:
        "200":
          description: Bundle updated; the returned bundle carries the new `version`.
        "400":
          description: "`bundle_workspace_required` / `bundle_version_required` — a browser session did not say which workspace or version its bundle came from."
        "409":
          description: "`bundle_workspace_mismatch` — the bundle was loaded from a different workspace than the session is in; `bundle_stale` — the row changed since the bundle was loaded. Re-read GET /api/projects and retry."
  /api/projects/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
    get:
      tags:
        - Projects
      summary: Get a single project
      x-app-scope: projects:read
      responses:
        "200":
          description: Project
        "404":
          description: Project not found
    delete:
      tags:
        - Projects
      summary: Delete a project and its resources
      responses:
        "200":
          description: Project deleted
        "404":
          description: Project not found
  /api/messages:
    get:
      tags:
        - Messages
      summary: Get all messages, contacts, and preferences
      responses:
        "200":
          description: Messages payload
    patch:
      tags:
        - Messages
      summary: Update chat preferences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                preferences:
                  type: object
                  properties:
                    activeStatus:
                      type: boolean
                    notificationSound:
                      type: boolean
                    doNotDisturb:
                      type: boolean
      responses:
        "200":
          description: Preferences updated
  /api/messages/groups:
    post:
      tags:
        - Messages
      summary: Create a group chat
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                memberIds:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Group created
  /api/messages/groups/{groupId}/members:
    parameters:
      - name: groupId
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Messages
      summary: Add a member to a group chat
      description: Adds a workspace member to a custom group chat. The caller must be a current group member; the target must be an active member of the workspace. Team-kind groups mirror workspace teams and reject membership edits. Adding an existing member is an idempotent no-op.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - userId
              properties:
                userId:
                  type: string
      responses:
        "201":
          description: Member added
        "400":
          description: Missing/invalid userId, or target not an active workspace member
        "403":
          description: Caller may not manage this group's membership
        "404":
          description: Group not found in this workspace
    delete:
      tags:
        - Messages
      summary: Leave a group chat or remove a member
      description: With no body (or your own userId), leaves the group — always allowed on custom groups. With another user's id (JSON body `userId` or the `userId` query parameter), removes that member — workspace admins or the group creator only. The removed user's thread history is preserved. Team-kind groups reject membership edits.
      parameters:
        - name: userId
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                userId:
                  type: string
      responses:
        "200":
          description: Membership removed (idempotent)
        "403":
          description: Caller may not remove that member
        "404":
          description: Group not found in this workspace
  /api/messages/threads/{contactId}:
    patch:
      tags:
        - Messages
      summary: Update thread flags (pin, mute, block, etc.)
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                pinned:
                  type: boolean
                muted:
                  type: boolean
                blocked:
                  type: boolean
                archived:
                  type: boolean
                hidden:
                  type: boolean
      responses:
        "200":
          description: Thread updated
    delete:
      tags:
        - Messages
      summary: Delete a conversation thread
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Thread deleted
  /api/messages/threads/{contactId}/members:
    get:
      tags:
        - Messages
      summary: List the members of a group-type chat (team chat, group, or channel)
      description: Returns who is in a workspace team chat, managed group chat, or channel. The caller must be a participant (private channels included). Direct-message threads return 404 — they have presence, not a member list.
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Members of the chat, caller first, then alphabetical
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  members:
                    type: array
                    items:
                      type: object
                      properties:
                        userId:
                          type: string
                        name:
                          type: string
                        isYou:
                          type: boolean
                  canAdd:
                    type: boolean
                    description: Caller may add members via the chat's members route
                  canRemoveOthers:
                    type: boolean
                    description: Caller may remove other members
                  canLeave:
                    type: boolean
                    description: Caller may leave this chat
        "403":
          description: Caller is not a participant of this chat
        "404":
          description: Not a group-type chat, or not in this workspace
  /api/messages/threads/{contactId}/messages:
    post:
      tags:
        - Messages
      summary: Send a message
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
                attachments:
                  type: array
                  items:
                    type: object
      responses:
        "200":
          description: Message sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  message:
                    $ref: "#/components/schemas/ChatMessage"
  /api/messages/threads/{contactId}/messages/{messageId}:
    patch:
      tags:
        - Messages
      summary: Edit a message
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
        - name: messageId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
      responses:
        "200":
          description: Message edited
    delete:
      tags:
        - Messages
      summary: Delete a message
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
        - name: messageId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Message deleted
  /api/messages/threads/{contactId}/messages/{messageId}/reactions:
    post:
      tags:
        - Messages
      summary: Toggle an emoji reaction on a message
      description: Toggles the caller's reaction for the given emoji — adds it when absent, removes it when present. The change fans out to every participant's copy of the message, so all participants see the same attributed reaction list. The caller must be a participant of the thread. Reactions are returned as `[{emoji, userIds}]`; a message carries at most 20 distinct emoji.
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
        - name: messageId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - emoji
              properties:
                emoji:
                  type: string
                  description: A single emoji (one grapheme, max 16 characters).
      responses:
        "200":
          description: Reaction toggled; returns the updated reaction list
        "400":
          description: Invalid emoji, or the distinct-reaction limit was reached
        "404":
          description: Message or conversation not found
  /api/messages/threads/{contactId}/typing:
    patch:
      tags:
        - Messages
      summary: Set typing state
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                typing:
                  type: boolean
      responses:
        "200":
          description: Typing state updated
  /api/messages/threads/{contactId}/meeting:
    get:
      tags:
        - Meetings
      summary: Get meeting session
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Meeting session data
    patch:
      tags:
        - Meetings
      summary: Update meeting controls or end/leave meeting
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                micEnabled:
                  type: boolean
                cameraEnabled:
                  type: boolean
                speakerEnabled:
                  type: boolean
                screenSharing:
                  type: boolean
                ended:
                  type: boolean
                left:
                  type: boolean
      responses:
        "200":
          description: Meeting updated
  /api/messages/threads/{contactId}/meeting/messages:
    post:
      tags:
        - Meetings
      summary: Send a meeting chat message
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                content:
                  type: string
      responses:
        "200":
          description: Meeting message sent
  /api/messages/threads/{contactId}/meeting/participants:
    post:
      tags:
        - Meetings
      summary: Add a meeting participant
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - participantId
              properties:
                participantId:
                  type: string
      responses:
        "200":
          description: Participant added
    patch:
      tags:
        - Meetings
      summary: Update a meeting participant (mute, kick)
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - participantId
              properties:
                participantId:
                  type: string
                micEnabled:
                  type: boolean
                kicked:
                  type: boolean
      responses:
        "200":
          description: Participant updated
  /api/messages/threads/{contactId}/meeting/signals:
    get:
      tags:
        - Meetings
      summary: Poll for WebRTC signals
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
        - name: after
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Signal list
    post:
      tags:
        - Meetings
      summary: Send a WebRTC signal
      description: Send one signal (`type` + `payload`) or a batch (`signals`, at most 100) to the same target. Each signal is stored as its own row, so the GET side is identical either way; the browser batches each burst of ICE candidates into one request.
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - targetUserId
              properties:
                targetUserId:
                  type: string
                type:
                  type: string
                  enum:
                    - offer
                    - answer
                    - ice-candidate
                payload:
                  type: object
                signals:
                  type: array
                  maxItems: 100
                  items:
                    type: object
                    required:
                      - type
                      - payload
                    properties:
                      type:
                        type: string
                        enum:
                          - offer
                          - answer
                          - ice-candidate
                      payload:
                        type: object
      responses:
        "200":
          description: Signal sent
  /api/people:
    get:
      tags:
        - People
      summary: Get teams, members, and projects
      description: "Each member carries a derived `invite` object, read from workspace memberships and invites rather than the stored roster: `status` is one of `joined`, `pending`, `awaiting_approval`, `expired`, `declined`, `removed`, `deactivated` or `not_invited`, with `inviteId`, `invitedAt` and `expiresAt` when an invite exists. It is never stored; echoing it back on PATCH is ignored."
      responses:
        "200":
          description: People workspace bundle
    patch:
      tags:
        - People
      summary: Update teams and members
      description: "Replaces the workspace's teams and members with the submitted lists. A project missing from `projects` is KEPT (this route never removes a project). `workspaceId` names the workspace the roster was loaded from and `version` the row version it was built on (GET returns both): 409 `bundle_workspace_mismatch` when the workspace differs, 409 with the fresh `bundle` when the row has moved on, 400 `bundle_workspace_required` / `bundle_version_required` when a browser session omits either. API-key callers may omit them; when they echo, they are checked. Only addresses this save ADDS to the roster are invited; removing or editing members never re-invites anyone. Removing a member here does not remove a joined member's workspace access (use DELETE /api/workspace/members/{userId})."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workspaceId:
                  type: string
                  description: The workspace the roster was loaded from. Required for browser sessions.
                version:
                  type: string
                  description: The `version` the roster was built on. Required for browser sessions.
                teams:
                  type: array
                  items:
                    type: object
                members:
                  type: array
                  items:
                    type: object
                projects:
                  type: array
                  items:
                    type: object
      responses:
        "200":
          description: People bundle updated
        "400":
          description: "`bundle_workspace_required` / `bundle_version_required`"
        "409":
          description: "`bundle_workspace_mismatch`, or the row changed since the roster was loaded (the body carries the fresh `bundle`)"
  /api/notifications:
    get:
      tags:
        - Notifications
      summary: Get notification center data
      responses:
        "200":
          description: Notifications data
    patch:
      tags:
        - Notifications
      summary: Update notification state
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required:
                    - action
                    - projectRef
                    - notificationId
                    - unread
                  properties:
                    action:
                      const: set-read
                    projectRef:
                      type: string
                    notificationId:
                      type: string
                    unread:
                      type: boolean
                - type: object
                  required:
                    - action
                    - projectRef
                    - notificationId
                    - snoozed
                  properties:
                    action:
                      const: set-snoozed
                    projectRef:
                      type: string
                    notificationId:
                      type: string
                    snoozed:
                      type: boolean
                - type: object
                  required:
                    - action
                    - projectRef
                    - notificationId
                  properties:
                    action:
                      const: remove
                    projectRef:
                      type: string
                    notificationId:
                      type: string
                - type: object
                  required:
                    - action
                  properties:
                    action:
                      const: mark-all-read
      responses:
        "200":
          description: Notification state updated
  /api/discussions/agent-reply:
    post:
      tags:
        - Discussions
      summary: Answer an @agent question in a discussion
      description: |
        Generates the AI agent's answer to a stored thread or reply and posts it as the agent. Idempotent per question. The reply notifies nobody.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - projectRef
                - threadId
              properties:
                projectRef:
                  type: string
                threadId:
                  type: string
                replyId:
                  type: string
                  description: The reply that mentions @agent; omit for the opening post.
      responses:
        "200":
          description: This question was already answered
        "201":
          description: The agent's reply, and the project's discussions including it
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  replyId:
                    type: string
                  discussions:
                    type: object
        "400":
          description: That post does not ask @agent anything
        "401":
          description: Unauthorized
        "403":
          description: The AI agent is disabled
          or the project is not visible: null
        "404":
          description: Thread or reply not found
        "429":
          description: Rate limited
          or the daily AI token budget is spent: null
        "502":
          description: The model could not answer
        "503":
          description: The AI agent is not configured
  /api/discussions:
    get:
      tags:
        - Discussions
      summary: Get discussions for a project
      x-app-scope: messages:read
      parameters:
        - name: projectRef
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Discussions list
    post:
      tags:
        - Discussions
      summary: Create a thread or reply (multipart form or JSON)
      x-app-scope: messages:write
      description: "Use multipart/form-data to attach files; application/json is accepted for text-only posts. When `mode` is omitted it is inferred: a body carrying `threadId` is a reply, one carrying `title` is a new thread."
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - mode
                - projectRef
              properties:
                mode:
                  type: string
                  enum:
                    - thread
                    - reply
                projectRef:
                  type: string
                title:
                  type: string
                body:
                  type: string
                threadId:
                  type: string
                content:
                  type: string
                authorName:
                  type: string
                authorInitials:
                  type: string
                authorTone:
                  type: string
                attachments:
                  type: array
                  items:
                    type: string
                    format: binary
          application/json:
            schema:
              type: object
              required:
                - projectRef
              properties:
                mode:
                  type: string
                  enum:
                    - thread
                    - reply
                projectRef:
                  type: string
                title:
                  type: string
                body:
                  type: string
                threadId:
                  type: string
                content:
                  type: string
                authorName:
                  type: string
                authorInitials:
                  type: string
                authorTone:
                  type: string
      responses:
        "201":
          description: Discussion created
        "400":
          description: Unparseable body, missing projectRef, or invalid write mode
    patch:
      tags:
        - Discussions
      summary: Toggle star or add reaction
      x-app-scope: messages:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                action:
                  type: string
                  enum:
                    - toggle-star
                    - add-reaction
                projectRef:
                  type: string
                threadId:
                  type: string
                starred:
                  type: boolean
                targetType:
                  type: string
                  enum:
                    - thread
                    - reply
                targetId:
                  type: string
                emoji:
                  type: string
      responses:
        "200":
          description: Discussion updated
    delete:
      tags:
        - Discussions
      summary: Delete a thread or reply
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - projectRef
                - targetType
                - targetId
              properties:
                projectRef:
                  type: string
                targetType:
                  type: string
                  enum:
                    - thread
                    - reply
                targetId:
                  type: string
      responses:
        "200":
          description: Discussion entry deleted
  /api/time-tracker:
    get:
      tags:
        - Time Tracker
      summary: Get time tracker dashboard
      x-app-scope: time_entries:read
      parameters:
        - name: actorName
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Time tracker dashboard
    post:
      tags:
        - Time Tracker
      summary: Start a timer
      x-app-scope: time_entries:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
                - projectRef
              properties:
                action:
                  const: start
                projectRef:
                  type: string
                taskId:
                  type: integer
                  nullable: true
                actorName:
                  type: string
      responses:
        "201":
          description: Timer started
    patch:
      tags:
        - Time Tracker
      summary: Stop a timer or update a time entry
      x-app-scope: time_entries:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
              properties:
                action:
                  type: string
                  enum:
                    - stop
                    - update
                entryId:
                  type: string
                actorName:
                  type: string
                projectRef:
                  type: string
                taskId:
                  type: integer
                  nullable: true
                startedAt:
                  type: string
                endedAt:
                  type: string
                  nullable: true
                billable:
                  type: boolean
                  description: Mark the entry billable (action=update only).
                hourlyRateCents:
                  type: integer
                  nullable: true
                  description: Per-entry hourly rate override in integer cents (action=update only). null clears the override so the project/client default applies. Entries already stamped onto an invoice reject updates.
      responses:
        "200":
          description: Timer stopped / entry updated
        "409":
          description: action=stop found the entry already closed. A timer can be stopped from the dashboard, the task panel, the Plan tab, the agent, another window or the 24h stale-timer cron, so losing this race is normal — re-read the dashboard rather than treating it as a failure.
    delete:
      tags:
        - Time Tracker
      summary: Delete a time entry
      parameters:
        - name: entryId
          in: query
          required: true
          schema:
            type: string
        - name: actorName
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Time entry deleted
        "400":
          description: entryId is required
  /api/time-tracker/billing/preview:
    get:
      tags:
        - Time Tracker
      summary: Preview unbilled billable time for a client as invoice line items
      description: "Admin only. Rolls a client's unbilled billable time entries into ClearBooks-ready line items grouped per project (rate resolution: entry override → project default → client default; entries with no resolvable rate or still-running timers are returned as skipped with a reason). Read-only — nothing is created or stamped. Amounts are integer cents."
      parameters:
        - name: clientId
          in: query
          required: true
          schema:
            type: string
        - name: from
          in: query
          description: ISO timestamp lower bound on started_at (inclusive).
          schema:
            type: string
        - name: to
          in: query
          description: ISO timestamp upper bound on started_at (inclusive).
          schema:
            type: string
        - name: projectRefs
          in: query
          description: Comma-separated subset of the client's project refs.
          schema:
            type: string
      responses:
        "200":
          description: Billable-time preview (line items, total, skipped entries)
        "400":
          description: Unknown client / project refs not belonging to the client
  /api/time-tracker/billing/invoice:
    post:
      tags:
        - Time Tracker
      summary: Create a draft ClearBooks invoice from unbilled billable time
      description: Admin only. Creates a DRAFT invoice in ClearBooks (never sends) from the same selection the preview shows, then stamps the billed entries with the invoice reference. Only unbilled entries are ever selected, so a retry cannot double-bill. Requires the calling admin's ClearBooks connection and a client → ClearBooks-customer mapping (stored on the client, or passed as clearbooksCustomerId — an explicit id is remembered on the client when it has no mapping yet).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - clientId
              properties:
                clientId:
                  type: string
                from:
                  type: string
                to:
                  type: string
                projectRefs:
                  type: array
                  items:
                    type: string
                clearbooksCustomerId:
                  type: string
                invoiceDate:
                  type: string
                  description: YYYY-MM-DD; ClearBooks defaults it server-side.
                dueDate:
                  type: string
                  description: YYYY-MM-DD; ClearBooks defaults it server-side.
                notes:
                  type: string
      responses:
        "201":
          description: Draft created; returns invoiceId + stamped entry count
        "400":
          description: Nothing to bill / missing customer mapping / not connected
        "502":
          description: ClearBooks rejected the invoice, or the draft was created but stamping failed (the response carries invoiceId so the draft is not orphaned silently)
  /api/time-tracker/billing/unbill:
    post:
      tags:
        - Time Tracker
      summary: Clear the invoice stamp from time entries (local inverse)
      description: Admin only. Clears invoice_ref/invoiced_at from every entry stamped with the given ClearBooks invoice reference so the time becomes billable again — the local inverse for a voided/deleted draft. Does NOT touch ClearBooks.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - invoiceRef
              properties:
                invoiceRef:
                  type: string
      responses:
        "200":
          description: Stamp cleared; returns clearedEntryCount
        "404":
          description: No entries carry that invoice reference
  /api/time-tracker/billing/rates:
    get:
      tags:
        - Time Tracker
      summary: Read default billing rates and the ClearBooks customer mapping
      description: Admin only. Returns every per-project default hourly rate in the workspace and, when clientId is passed, that client's default rate and ClearBooks customer mapping.
      parameters:
        - name: clientId
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Billing rate settings
    patch:
      tags:
        - Time Tracker
      summary: Set default billing rates / the ClearBooks customer mapping
      description: Admin only. Partial update — only fields present in the body are written. Rates are positive integer cents; null clears a rate (or the mapping).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                clientId:
                  type: string
                clientDefaultHourlyRateCents:
                  type: integer
                  nullable: true
                clearbooksCustomerId:
                  type: string
                  nullable: true
                projectRates:
                  type: array
                  items:
                    type: object
                    required:
                      - projectRef
                    properties:
                      projectRef:
                        type: string
                      defaultHourlyRateCents:
                        type: integer
                        nullable: true
      responses:
        "200":
          description: Updated settings summary
        "400":
          description: Invalid rate / no fields to update
        "404":
          description: Client not found
  /api/project-files:
    get:
      tags:
        - Files
      summary: List project files
      x-app-scope: files:read
      parameters:
        - name: projectRef
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: File list
    post:
      tags:
        - Files
      summary: Upload files to a project
      x-app-scope: files:write
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - projectRef
                - files
              properties:
                projectRef:
                  type: string
                documentId:
                  type: string
                files:
                  type: array
                  items:
                    type: string
                    format: binary
      responses:
        "201":
          description: Files uploaded
    patch:
      tags:
        - Files
      summary: Rename or duplicate a file
      x-app-scope: files:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
                - projectRef
                - fileId
              properties:
                action:
                  type: string
                  enum:
                    - rename
                    - duplicate
                projectRef:
                  type: string
                fileId:
                  type: string
                name:
                  type: string
      responses:
        "200":
          description: File updated
    delete:
      tags:
        - Files
      summary: Delete a project file
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - projectRef
                - fileId
              properties:
                projectRef:
                  type: string
                fileId:
                  type: string
      responses:
        "200":
          description: File deleted
  /api/activity:
    get:
      tags:
        - Activity
      summary: Get workspace activity feed
      responses:
        "200":
          description: Activity feed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  activity:
                    type: array
                    items:
                      $ref: "#/components/schemas/ActivityItem"
    post:
      tags:
        - Activity
      summary: Append an activity item
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - item
              properties:
                item:
                  $ref: "#/components/schemas/ActivityItem"
      responses:
        "200":
          description: Activity appended
  /api/profile:
    get:
      tags:
        - Profile
      summary: Get current user profile
      responses:
        "200":
          description: User profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  profile:
                    $ref: "#/components/schemas/ProfileFormState"
    patch:
      tags:
        - Profile
      summary: Update user profile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProfileFormState"
      responses:
        "200":
          description: Profile updated
  /api/settings:
    get:
      tags:
        - Settings
      summary: Get user settings bundle
      responses:
        "200":
          description: Settings bundle
    patch:
      tags:
        - Settings
      summary: Update user settings
      description: "A PATCH that touches the workspace half (`workspace`, `projectTags`, `projectTypes`) is admin-only and must name the workspace its form was loaded from as `workspaceId` (GET returns it): 409 `bundle_workspace_mismatch` when it differs, 400 `bundle_workspace_required` when a browser session omits it. The per-user half (`notifications`, `plan`, `devices`) is not gated."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workspaceId:
                  type: string
                  description: The workspace the settings were loaded from. Required for browser sessions writing workspace settings.
      responses:
        "200":
          description: Settings updated
        "400":
          description: "`bundle_workspace_required`"
        "409":
          description: "`bundle_workspace_mismatch`"
  /api/settings/password:
    get:
      tags:
        - Settings
      summary: Whether the account has a password set
      description: Returns `{ hasPassword }`. False for OAuth-only accounts, which use this to offer setting a password without a current one.
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Settings
      summary: Change password
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - currentPassword
                - newPassword
                - confirmPassword
              properties:
                currentPassword:
                  type: string
                newPassword:
                  type: string
                  minLength: 8
                confirmPassword:
                  type: string
      responses:
        "200":
          description: Password changed
        "401":
          description: Current password incorrect
  /api/settings/account:
    delete:
      tags:
        - Settings
      summary: Delete user account
      responses:
        "200":
          description: Account deleted
  /api/settings/workspace:
    delete:
      tags:
        - Settings
      summary: Delete workspace (owner only)
      responses:
        "200":
          description: Workspace deleted
        "403":
          description: Not the workspace owner
  /api/settings/security:
    get:
      tags:
        - Settings
      summary: Get security info (active sessions)
      responses:
        "200":
          description: Security info
    post:
      tags:
        - Settings
      summary: Security action (e.g. revoke session)
      responses:
        "200":
          description: Action completed
  /api/settings/extension-shield:
    get:
      tags:
        - Settings
      summary: Is Extension Shield on for this workspace
      description: |
        Whether the workspace has the `extension_shield_enabled` security policy turned on. Read-only and visible to every member — the shield is a client-side containment mode each member's browser runs, not an admin secret. Writing the policy goes through `POST /api/admin/security/policies` (admin only).
      responses:
        "200":
          description: Shield state
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  enabled:
                    type: boolean
  /api/settings/extension-shield/detections:
    post:
      tags:
        - Settings
      summary: Record a browser-extension detection for the caller
      description: |
        Sent by the in-app extension guard once per detected extension per page load, so support can see what a member's browser injected without asking them to open DevTools. Filed under the caller's own user and workspace (never anything in the body) in `app_extension_detections`; a repeat from the same browser bumps `seen_count`. Identity only — the element's tag/id/class and the extension origin, never page contents. `name` must be one the guard can produce (a known marker name or "Unidentified browser extension"); `listedAs` is derived server-side from it.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  example: Glasp
                origin:
                  type: string
                  description: "`chrome-extension://<id>`-style origin, when the match carried one."
                tag:
                  type: string
                id:
                  type: string
                class:
                  type: string
                shieldDevice:
                  type: boolean
                warningSuppressed:
                  type: boolean
      responses:
        "200":
          description: Recorded (or `recorded:false` when the caller has no workspace)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  recorded:
                    type: boolean
        "400":
          description: Not a detection report the guard could have sent
        "413":
          description: Payload too large
        "429":
          description: Rate limited
  /api/automations/vocabulary:
    get:
      tags:
        - Automations
      summary: Every trigger and action a rule can be built from
      description: |
        The full vocabulary: each trigger with the conditions it accepts, and each action with the config fields it needs, whether it requires a task, and whether its effect leaves Kavanah irreversibly.

        DERIVED from `ACTION_HANDLERS` and `TRIGGER_TYPES` — the same registries `POST /api/automations` validates against — rather than restated. The rule builder that consumes it therefore cannot offer a trigger the server rejects, and cannot silently miss one that ships later. The builder it replaced kept its own copy, could express 4 of the 14 triggers, and always saved an empty action config.
      responses:
        "200":
          description: Trigger and action vocabulary
          content:
            application/json:
              schema:
                type: object
                properties:
                  triggers:
                    type: array
                    items:
                      type: object
                      properties:
                        type:
                          type: string
                        label:
                          type: string
                        description:
                          type: string
                        carriesTask:
                          type: boolean
                          description: False for event triggers, which only pair with task-free actions.
                        conditions:
                          type: array
                          items:
                            $ref: "#/components/schemas/AutomationConfigField"
                  actions:
                    type: array
                    items:
                      type: object
                      properties:
                        type:
                          type: string
                        label:
                          type: string
                        description:
                          type: string
                        requiresTask:
                          type: boolean
                        externalEffect:
                          type: boolean
                          description: Reaches outside Kavanah irreversibly; admin-only to create.
                        fields:
                          type: array
                          items:
                            $ref: "#/components/schemas/AutomationConfigField"
  /api/deals/audience:
    get:
      tags:
        - Deals
      summary: This workspace's outbound opt-out list
      description: |
        Addresses this workspace must not email. Member-level: anyone about to email someone needs to be able to check.

        Note what is NOT here. The platform-wide suppression list (hard bounces and spam complaints) is keyed by address alone and shared across every tenant, because those are facts about the address and about the sending infrastructure. This endpoint returns only the opt-outs this workspace recorded — a campaign opt-out is said to one desk, and putting it in the global list would silently stop mail to that address from every other workspace on the platform.
      responses:
        "200":
          description: Opt-out list
          content:
            application/json:
              schema:
                type: object
                properties:
                  suppressions:
                    type: array
                    items:
                      type: object
                      properties:
                        email:
                          type: string
                        reason:
                          type: string
                        source:
                          type: string
                          description: angelsend | manual | reply | a provider name
                        note:
                          type: string
                          nullable: true
                        createdAt:
                          type: string
                          format: date-time
    post:
      tags:
        - Deals
      summary: Suppress an address, or reconcile the audience with AngelSend
      description: |
        Admin only — including `unsuppress`, which re-permits mail to someone who asked us to stop.

        `action: "sync"` runs ONE `direction`. They are separate on purpose and running one is not "syncing the audience":
        * `optouts_in` — pull AngelSend's do-not-email list into this
          workspace's. The safety-critical one: until it has run, a person who
          unsubscribed from an AngelSend campaign still receives Kavanah cadence
          steps, because the two lists never met.

        * `optouts_out` — push this workspace's own opt-outs to AngelSend.
        * `contacts_in` — pull an AngelSend segment (or the whole lead list)
          into the Deal Room. Gap-filling: it never overwrites a field somebody
          curated here.

        * `contacts_out` — push Deal Room contacts into an AngelSend project.
          This direction OVERWRITES whole AngelSend rows, because `import-leads`
          has no partial form; a field Kavanah does not hold is blanked there.
          Opted-out and bounced addresses are excluded, since importing one is
          how an opt-out gets quietly undone.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
              properties:
                action:
                  type: string
                  enum:
                    - suppress
                    - unsuppress
                    - sync
                email:
                  type: string
                  description: Required for suppress / unsuppress.
                reason:
                  type: string
                note:
                  type: string
                direction:
                  type: string
                  enum:
                    - optouts_in
                    - optouts_out
                    - contacts_in
                    - contacts_out
                  description: Required for sync.
                projectId:
                  type: string
                  description: AngelSend project. Required for contacts_out.
                segmentId:
                  type: string
                  description: Segment to pull for contacts_in; omit for the whole lead list.
                limit:
                  type: integer
                  description: Rows to pull for contacts_in. Default 200, capped at 500.
      responses:
        "200":
          description: Done, with per-direction counts for a sync
        "400":
          description: Unknown action or direction
          or a missing required field: null
        "403":
          description: Not a workspace admin
  /api/deals/contact-runway:
    get:
      tags:
        - Deals
      summary: How long each live network cadence can keep reaching new people
      description: |
        Per enabled NETWORK cadence: sendable contacts never enrolled in it (has an email, not archived, not suppressed at either level), contacts enrolled but not yet emailed, the measured pace of FIRST emails over the last week (capped by today's send capacity), and the days that leaves. A cadence that sent no first email in the window has no runway clock and reports null rather than zero. Also returns the warning threshold and when admins were last warned. A workspace with no live network cadence gets an empty list and is never warned — that is the opt-in.
      responses:
        "200":
          description: Runway per cadence, plus the threshold and last warning
          content:
            application/json:
              schema:
                type: object
                properties:
                  runway:
                    type: object
                    properties:
                      workspaceId:
                        type: string
                      thresholdDays:
                        type: integer
                      capacityPerDay:
                        type: number
                      cadences:
                        type: array
                        items:
                          type: object
                          properties:
                            cadenceId:
                              type: string
                            name:
                              type: string
                            pool:
                              type: integer
                            awaitingFirstTouch:
                              type: integer
                            firstTouches:
                              type: integer
                            windowDays:
                              type: number
                            newContactsPerDay:
                              type: number
                            runwayDays:
                              type: number
                              nullable: true
                      low:
                        type: array
                        description: The cadences below the threshold, shortest runway first.
                        items:
                          type: object
                  lastAlert:
                    type: object
                    nullable: true
                    properties:
                      alertedAt:
                        type: string
                        format: date-time
                      runwayDays:
                        type: number
                        nullable: true
        "400":
          description: No workspace
    patch:
      tags:
        - Deals
      summary: Set the contact-runway warning threshold (admin)
      description: |
        Admin only: the threshold decides when every workspace admin is emailed. Whole days, 1-90, default 7.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                thresholdDays:
                  type: integer
                  minimum: 1
                  maximum: 90
              required:
                - thresholdDays
      responses:
        "200":
          description: Saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  thresholdDays:
                    type: integer
        "400":
          description: thresholdDays is not a whole number of days in range
        "403":
          description: Not a workspace admin
  /api/deals/cadences/{id}/experiment:
    get:
      tags:
        - Deals
      summary: A cadence's subject/sender experiment and winner policy
      description: |
        The cadence's design (`linked` — one hash picks the arm, so subject and sender are confounded — or `factorial` — subject and sender assigned by independent hashes), per-factor outcome stats per contact (replies, positive replies, meetings; opens are not tracked), each level's Bayesian probability to be best, the automatic winner policy and its current verdict, and the recent winner decisions. Member-level.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Cadence id.
      responses:
        "200":
          description: The experiment report
          content:
            application/json:
              schema:
                type: object
                properties:
                  experiment:
                    type: object
                    additionalProperties: true
        "404":
          description: Cadence not found
    patch:
      tags:
        - Deals
      summary: Configure a cadence's experiment design and automatic winner policy
      description: |
        Admin only. `autoWinnerEnabled` (default false) needs `design: factorial`; once on, new enrollments shift to a declared winner while `explorePct` of them stay evenly split. A contact's subject and sender are fixed at their first send and are never changed by this call. Turning the policy off, changing the metric, or `resetWinner` clears the verdict and records it in the decision log.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Cadence id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                design:
                  type: string
                  enum:
                    - linked
                    - factorial
                autoWinnerEnabled:
                  type: boolean
                metric:
                  type: string
                  enum:
                    - reply
                    - positive_reply
                    - meeting
                minPerArm:
                  type: integer
                  minimum: 30
                  maximum: 10000
                explorePct:
                  type: integer
                  minimum: 5
                  maximum: 50
                threshold:
                  type: number
                  minimum: 0.9
                  maximum: 0.999
                resetWinner:
                  type: string
                  enum:
                    - subject
                    - sender
                    - all
      responses:
        "200":
          description: What changed, and the updated report
          content:
            application/json:
              schema:
                type: object
                properties:
                  changes:
                    type: array
                    items:
                      type: string
                  experiment:
                    type: object
                    additionalProperties: true
        "400":
          description: Invalid value
          or auto-winner requested without the factorial design: null
        "403":
          description: Not a workspace admin
        "404":
          description: Cadence not found
        "409":
          description: The experiment kept changing underneath the write (the hourly evaluator or another admin); re-read and retry
  /api/deals/cadences/{id}/enroll-bulk:
    post:
      tags:
        - Deals
      summary: Enroll up to 200 contacts in one cadence
      description: |
        Enrolls each contact through the same checks as a single enrollment — the cadence's kind must match (a deal cadence needs `dealId`, a network cadence must not have one), the contact must belong to this workspace, and a contact already in an active sequence on the deal (or in another network cadence) is reported rather than enrolled twice. An address on the workspace's or the platform's do-not-email list is reported as `suppressed` and not enrolled.

        This ARMS UNATTENDED OUTBOUND EMAIL: every enrolled contact receives each step from the caller's own connected mailbox until the sequence ends, they reply, or someone stops it.

        For a deal cadence the caller needs edit access on the deal, and each contact must be on the deal, because stop-on-reply follows the deal's contacts. `addToDeal: true` links the ones who are not (role `other`) first; without it they come back as `not_on_deal`. More than 200 contacts is refused with nothing enrolled — never truncated.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Cadence id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contactIds
              properties:
                contactIds:
                  type: array
                  minItems: 1
                  maxItems: 200
                  items:
                    type: string
                    format: uuid
                dealId:
                  type: string
                  format: uuid
                  description: Required for a deal cadence; must be omitted for a network cadence.
                addToDeal:
                  type: boolean
                  description: Link contacts not yet on the deal before enrolling them. Default false.
                provider:
                  type: string
                  enum:
                    - auto
                    - gmail
                    - angelsend
      responses:
        "200":
          description: Per-contact outcomes (a partial success is still 200)
          content:
            application/json:
              schema:
                type: object
                properties:
                  cadenceId:
                    type: string
                  dealId:
                    type: string
                    nullable: true
                  enrolled:
                    type: integer
                  summary:
                    type: string
                  outcomes:
                    type: array
                    items:
                      type: object
                      properties:
                        contactId:
                          type: string
                        name:
                          type: string
                          nullable: true
                        email:
                          type: string
                          nullable: true
                        status:
                          type: string
                          enum:
                            - enrolled
                            - already_enrolled
                            - not_on_deal
                            - no_email
                            - archived
                            - suppressed
                            - not_found
                            - duplicate
                            - error
                        detail:
                          type: string
                        enrollmentId:
                          type: string
                        firstSendAt:
                          type: string
                          nullable: true
                        linked:
                          type: boolean
        "400":
          description: Missing or oversized contactIds, or dealId that does not fit the cadence's kind
        "403":
          description: No edit access on the deal
        "404":
          description: Cadence or deal not found
        "409":
          description: Cadence disabled or empty
          or the deal is closed: null
  /api/deals/forecast:
    get:
      tags:
        - Deals
      summary: The Deal Room revenue forecast, calculated live
      description: |
        Computed from the deal records as the caller sees them — private deals they cannot open, and their sums, are left out. Probability is the stage's baseline probability × momentum ÷ 50 (momentum 50 is neutral, built from counterparty activity on each deal's timeline, time in stage and a passed close date). Revenue is each deal's recorded fee, or deal size × the workspace's assumed fee % where none is recorded. When the workspace reports fee bands separately, every total is per band and no combined figure is present. Not cached: the recalculate and settings routes beside it change what it returns.
      parameters:
        - name: pipeline
          in: query
          required: false
          schema:
            type: string
          description: Pipeline id; omit (or __all) for every pipeline.
        - name: tz
          in: query
          required: false
          schema:
            type: string
          description: IANA timezone deciding what 'this week' means. Defaults to the caller's.
      responses:
        "200":
          description: Forecast report
          content:
            application/json:
              schema:
                type: object
                properties:
                  report:
                    type: object
                    description: "DealForecastReport (src/lib/deal-forecast/engine.ts): weeks, overdue, later, unscheduled, stages, confidence, risks, upside, lines, coverage, settings, bandKeys."
                  timezone:
                    type: string
                  pipelines:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                  settings:
                    type: object
                    properties:
                      autoRecalculateEnabled:
                        type: boolean
                      lastRecalculatedAt:
                        type: string
                        format: date-time
                        nullable: true
                  viewer:
                    type: object
                    properties:
                      isAdmin:
                        type: boolean
        "400":
          description: The pipeline parameter is not a pipeline id
  /api/deals/forecast/settings:
    get:
      tags:
        - Deals
      summary: Forecast settings and the weekly forecast email schedule
      responses:
        "200":
          description: Current configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  config:
                    type: object
                    description: "{ settings: { autoRecalculateEnabled, defaultFeePct, bandThresholdCents, lowerBandLabel, upperBandLabel, disclaimer, lastRecalculatedAt }, email: null | { enabled, day, time, timezone, recipients[], subjectTemplate, subjectPreview, lastSentAt, nextRunAt } }"
    put:
      tags:
        - Deals
      summary: Configure the forecast (admin)
      description: |
        Admin only. Only the fields sent change — including `email.enabled`, which keeps its stored value when omitted, and the schedule's cron, which is kept when neither `day` nor `time` is sent. Automatic daily recalculation writes the forecast onto every open deal's forecast fields (never its own probability). The weekly email goes to active non-guest workspace members only, computed as the report's creator sees Deal Room; any other recipient is refused. Send `expectedWorkspaceId` (the `workspaceId` the GET returned) to have a save refused with 409 when the caller's active workspace has changed since.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                expectedWorkspaceId:
                  type: string
                  description: The workspace the draft was loaded for.
                settings:
                  type: object
                  properties:
                    autoRecalculateEnabled:
                      type: boolean
                    defaultFeePct:
                      type: number
                      description: Percent, 0–100.
                    bandThresholdDollars:
                      type: number
                      nullable: true
                    lowerBandLabel:
                      type: string
                      nullable: true
                    upperBandLabel:
                      type: string
                      nullable: true
                    disclaimer:
                      type: string
                      nullable: true
                email:
                  type: object
                  properties:
                    enabled:
                      type: boolean
                    day:
                      type: string
                      enum:
                        - sunday
                        - monday
                        - tuesday
                        - wednesday
                        - thursday
                        - friday
                        - saturday
                    time:
                      type: string
                      description: HH:MM, 24h, local to timezone.
                    timezone:
                      type: string
                    recipients:
                      type: array
                      items:
                        type: string
                        format: email
                    subjectTemplate:
                      type: string
                      nullable: true
      responses:
        "200":
          description: Updated configuration
        "400":
          description: Validation error (e.g. a recipient who is not a member, or a threshold sent in cents)
        "403":
          description: Not a workspace admin
        "409":
          description: The settings were loaded for a different workspace than the caller's active one
  /api/deals/forecast/recalculate:
    post:
      tags:
        - Deals
      summary: Recalculate the forecast onto every open deal now (admin)
      description: |
        The same write the daily job makes: forecast probability, momentum, expected fee, close week and confidence on every open deal, with a timeline entry on each deal whose forecast moved. Returns counts only. Runs are serialized per workspace and read "today" in the workspace's timezone.
      responses:
        "200":
          description: Recalculated
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    type: object
                    properties:
                      dealsUpdated:
                        type: integer
                      dealsChanged:
                        type: integer
                      closedCleared:
                        type: integer
                      recalculatedAt:
                        type: string
                        format: date-time
                      dealLimitReached:
                        type: boolean
        "403":
          description: Not a workspace admin
        "503":
          description: The database's deal triggers predate the forecast; run the migrations
  /api/deals/forecast/preview-email:
    post:
      tags:
        - Deals
      summary: Email the weekly forecast to yourself for review
      description: |
        Sends the forecast email, exactly as the schedule would build it, to the caller's own address only — built with the caller's Deal Room visibility. Rate-limited per member.
      responses:
        "200":
          description: Sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  sentTo:
                    type: string
                  subject:
                    type: string
        "429":
          description: Too many previews in the last hour
        "502":
          description: The forecast was built but the email could not be sent
  /api/settings/send-limits:
    get:
      tags:
        - Settings
      summary: Outbound send capacity, and whether a target is sustainable
      description: |
        Plan the outbound mailbox pool against a daily target. Returns every mailbox that could carry load (a configured limit row, a connected mail provider, or queued mail in the last 30 days), what the pool can send today after warm-up ramps, what it reaches at steady state and when, what the underlying providers safely support, and how many more mailboxes a target needs.

        The distinction this exists to make: raising a cap is one setting and takes a second, but SUSTAINING the volume is a property of the pool. A single mailbox configured at 1,000/day is refused by Google at roughly half that, and the reputation damage outlasts the campaign — so 1,000/day is planned as four mailboxes, not one turned up.

        Member-level: anyone planning a campaign needs these numbers and none of them are sensitive.
      parameters:
        - in: query
          name: target
          schema:
            type: integer
            minimum: 0
          description: Daily volume to plan against. Omit for a plain capacity report.
      responses:
        "200":
          description: Capacity plan
          content:
            application/json:
              schema:
                type: object
                properties:
                  defaultDailyCap:
                    type: integer
                  plan:
                    type: object
                    properties:
                      targetPerDay:
                        type: integer
                      todayCeiling:
                        type: integer
                      steadyStateCeiling:
                        type: integer
                      providerSafeCeiling:
                        type: integer
                      daysToSteadyState:
                        type: integer
                      shortfallAtSteadyState:
                        type: integer
                      additionalMailboxesNeeded:
                        type: integer
                        nullable: true
                      findings:
                        type: array
                        items:
                          type: string
                      senders:
                        type: array
                        items:
                          type: object
                          properties:
                            userId:
                              type: string
                            name:
                              type: string
                              nullable: true
                            email:
                              type: string
                              nullable: true
                            provider:
                              type: string
                              nullable: true
                            dailyCap:
                              type: integer
                            todayCap:
                              type: integer
                            warmingUp:
                              type: boolean
                            daysToFullCap:
                              type: integer
                            providerCeiling:
                              type: integer
                            overProvisioned:
                              type: boolean
    put:
      tags:
        - Settings
      summary: Set a mailbox's daily send cap, or the workspace default
      description: |
        Admin only. Omit `userId` to set the workspace default that every mailbox without its own row inherits.

        This ARMS higher unattended sending — the scheduled-email cron defers against this number every minute, so raising it immediately releases mail that was being held back. Prefer a warm-up ramp (`warmupStart` + `warmupFloor` + `warmupStep`) over a straight raise on any identity not already sending at the new volume: a domain that jumps to its cap on day one gets filtered, and the fix for that is a new domain rather than a smaller number next week.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - dailyCap
              properties:
                dailyCap:
                  type: integer
                  minimum: 0
                userId:
                  type: string
                  nullable: true
                  description: Must be an active member of this workspace. Omit for the workspace default.
                warmupStart:
                  type: string
                  nullable: true
                  description: YYYY-MM-DD the ramp begins; null clears the ramp.
                warmupFloor:
                  type: integer
                  description: Day-one ceiling. Default 50.
                warmupStep:
                  type: integer
                  description: Added per day until dailyCap. Default 25.
      responses:
        "200":
          description: Updated, with a refreshed capacity plan
        "400":
          description: Invalid cap
          date: null
          or a user outside this workspace: null
        "403":
          description: Not a workspace admin
  /api/blueprints:
    get:
      tags:
        - Settings
      summary: List blueprints and what drafting one costs
      description: |
        Owner/admin only. A Blueprint is an operating prompt turned into one reviewable plan of items mapped to existing capabilities (docs/agent-notes/blueprint.md). Returns the workspace's blueprints, newest first, and `draftEstimateCredits` — the planner call's price for a prompt of `promptChars` characters. `no-store`.
      parameters:
        - in: query
          name: promptChars
          schema:
            type: integer
            minimum: 0
          description: Prompt length to price the draft for (default 8000).
      responses:
        "200":
          description: Blueprints
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  draftEstimateCredits:
                    type: integer
                  blueprints:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        title:
                          type: string
                        version:
                          type: integer
                        itemCount:
                          type: integer
                        updatedAt:
                          type: string
        "403":
          description: Not a workspace owner or admin
    post:
      tags:
        - Settings
      summary: Draft a blueprint from an operating prompt (one billed planning call)
      description: |
        Owner/admin only. ONE Sonnet planning call, billed to the signed-in user (402 `insufficient_credits` when they cannot cover the `blueprint_draft` floor). Every proposed item is validated against the real registries (automation triggers/actions, agent switches, org and cadence templates, models); anything that does not validate is listed as unsupported, never kept. A prompt over 40,000 characters is clipped and the plan says so. With `blueprintId` the plan becomes that blueprint's next version. Creates nothing in the workspace.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - prompt
              properties:
                prompt:
                  type: string
                blueprintId:
                  type: string
                  format: uuid
                title:
                  type: string
      responses:
        "201":
          description: Drafted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  blueprint:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      title:
                        type: string
                      version:
                        type: integer
        "400":
          description: Missing prompt
        "402":
          description: Not enough credits for the planning call
        "403":
          description: Not a workspace owner or admin
        "404":
          description: blueprintId not found in this workspace
        "429":
          description: Rate limited
        "502":
          description: The planner could not produce a plan
  /api/blueprints/{id}:
    get:
      tags:
        - Settings
      summary: A blueprint with live blockers, apply state, diff and cost totals
      description: |
        Owner/admin only. The plan (items grouped, each with its quoted source sentence, monthly cost if turned on, compliance notes and LIVE blockers from the God Mode setup checklist), the apply ledger's state per item, the diff of the plan against what is applied (added / changed / removed — removal is only ever proposed), totals, applies and versions. `no-store`.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The blueprint
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  blueprint:
                    type: object
                  plan:
                    type: object
                  states:
                    type: object
                  diff:
                    type: array
                    items:
                      type: object
                  totals:
                    type: object
                  connections:
                    type: array
                    items:
                      type: object
                  applies:
                    type: array
                    items:
                      type: object
                  versions:
                    type: array
                    items:
                      type: object
                  modelOptions:
                    type: array
                    items:
                      type: object
                  isOwner:
                    type: boolean
        "403":
          description: Not a workspace owner or admin
        "404":
          description: Not found in this workspace
    patch:
      tags:
        - Settings
      summary: Toggle, tick "turn on now" or edit items
      description: |
        Owner/admin only. Every params edit is re-validated by the same validator as a draft and re-priced; an item's key never changes. `expectedUpdatedAt` (the blueprint's `updatedAt`) is required; a stale one is 409 `blueprint_stale`.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - expectedUpdatedAt
                - edits
              properties:
                expectedUpdatedAt:
                  type: string
                edits:
                  type: array
                  items:
                    type: object
                    required:
                      - key
                    properties:
                      key:
                        type: string
                      selected:
                        type: boolean
                      turnOn:
                        type: boolean
                      params:
                        type: object
      responses:
        "200":
          description: Saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  updatedAt:
                    type: string
        "400":
          description: Unknown item or an invalid edit
        "403":
          description: Not a workspace owner or admin
        "404":
          description: Not found in this workspace
        "409":
          description: The blueprint changed since it was loaded
  /api/blueprints/{id}/apply:
    post:
      tags:
        - Settings
      summary: Apply selected items, created switched off
      description: |
        Owner/admin only. Creates each item through its existing write path, DISARMED: schedules paused, rules and cadences off, meeting series disarmed, campaigns not started, switches unchanged. An item in `turnOn` is switched on only when `confirm[key]` equals the monthly cost the server computes now (null for usage-dependent items) — otherwise 409 `turn_on_unconfirmed` and nothing is applied. Idempotent per item key; failures are recorded per item (`partial`) and retried by applying again.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                itemKeys:
                  type: array
                  items:
                    type: string
                turnOn:
                  type: array
                  items:
                    type: string
                confirm:
                  type: object
                  additionalProperties:
                    type:
                      - integer
                      - "null"
      responses:
        "200":
          description: Applied (possibly partially)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  applyId:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum:
                      - completed
                      - partial
                      - failed
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        outcome:
                          type: string
                          enum:
                            - created
                            - armed
                            - skipped
                            - failed
                            - proposed
                            - manual
                        detail:
                          type: string
        "400":
          description: Unknown item keys or nothing selected
        "403":
          description: Not a workspace owner or admin
        "404":
          description: Not found in this workspace
        "409":
          description: A turn-on was not confirmed at the current cost
        "429":
          description: Rate limited
  /api/blueprints/{id}/revert:
    post:
      tags:
        - Settings
      summary: Revert what a blueprint applied, as a set
      description: |
        Owner/admin only. `{applyId}` reverts one apply, `{itemKeys}` specific items, neither everything still live — newest first. Removes what the blueprint created and restores prior switch values, but KEEPS (and reports) anything changed since the apply: an edited schedule, an armed meeting series, a cadence with enrollments, a switch someone changed back.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                applyId:
                  type: string
                  format: uuid
                itemKeys:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Reverted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  reverted:
                    type: integer
                  kept:
                    type: integer
                  results:
                    type: array
                    items:
                      type: object
        "403":
          description: Not a workspace owner or admin
        "404":
          description: Blueprint or apply not found in this workspace
  /api/settings/god-mode-setup:
    get:
      tags:
        - Settings
      summary: The God Mode setup checklist (live status of everything the deal machine needs)
      description: |
        Read-only. Each item carries a status (`ok`, `warn`, `missing`, `unknown` = could not check — never a pass, `info` = optional), a plain-language reason, and the actions that fix it (links to existing pages and OAuth starts, or an inline fix that calls an existing write route). Owners and admins get the whole workspace; any other member gets ONLY their own connections (their Google accounts and AngelSend) — another member's connection rows are never read for them. Includes a live DNS check of the sending domain's SPF/DKIM/DMARC. `no-store`.
      parameters:
        - in: query
          name: target
          schema:
            type: integer
            minimum: 0
          description: Daily send volume to plan sending capacity against. Omit for a plain capacity report.
      responses:
        "200":
          description: The checklist
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  workspaceId:
                    type: string
                    description: Echo for the inline compliance fix's tenant check.
                  isOwner:
                    type: boolean
                  status:
                    type: object
                    properties:
                      generatedAt:
                        type: string
                        format: date-time
                      viewer:
                        type: object
                        properties:
                          userId:
                            type: string
                          isAdmin:
                            type: boolean
                      progress:
                        type: object
                        properties:
                          done:
                            type: integer
                          total:
                            type: integer
                      items:
                        type: array
                        items:
                          type: object
                          properties:
                            key:
                              type: string
                            group:
                              type: string
                              enum:
                                - accounts
                                - compliance
                                - data
                                - automation
                                - ai_team
                            title:
                              type: string
                            status:
                              type: string
                              enum:
                                - ok
                                - warn
                                - missing
                                - unknown
                                - info
                            reason:
                              type: string
                            details:
                              type: array
                              items:
                                type: string
                            actions:
                              type: array
                              items:
                                type: object
                            members:
                              type: array
                              items:
                                type: object
                            records:
                              type: array
                              items:
                                type: object
                            flags:
                              type: array
                              items:
                                type: object
    post:
      tags:
        - Settings
      summary: Send a member an in-app setup reminder
      description: |
        Admin only. Writes ONE in-app alert (dashboard + bell) asking an active member of this workspace to connect their own account. Never emails. Deduped per (member, item) for 24 hours — a repeat returns `created: false` rather than claiming a reminder was sent.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - userId
                - itemKey
              properties:
                userId:
                  type: string
                  format: uuid
                itemKey:
                  type: string
                  enum:
                    - google_accounts
                    - angelsend
      responses:
        "200":
          description: Reminder written (or already pending)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  created:
                    type: boolean
                  message:
                    type: string
        "400":
          description: Unknown item
          malformed id: null
          or the caller themself: null
        "403":
          description: Not a workspace admin
        "404":
          description: Not an active member of this workspace
  /api/settings/trash:
    get:
      tags:
        - Settings
      summary: List trashed items
      responses:
        "200":
          description: Trash items
    post:
      tags:
        - Settings
      summary: Restore a trashed item
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                action:
                  const: restore
                trashId:
                  type: string
      responses:
        "200":
          description: Item restored
    delete:
      tags:
        - Settings
      summary: Permanently delete a trashed item
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                trashId:
                  type: string
      responses:
        "200":
          description: Item permanently deleted
  /api/auth/workspace-setup:
    get:
      tags:
        - Auth
      summary: Get workspace setup state
      responses:
        "200":
          description: Setup state
        "401":
          description: Not authenticated
    post:
      tags:
        - Auth
      summary: Complete workspace setup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workspaceName:
                  type: string
                invites:
                  type: array
                  items:
                    type: string
                    format: email
      responses:
        "200":
          description: Setup completed
  /api/knowledge/documents:
    get:
      tags:
        - Knowledge
      summary: List knowledge documents
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 1000
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
      responses:
        "200":
          description: Documents
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  documents:
                    type: array
                    items:
                      $ref: "#/components/schemas/KnowledgeDocument"
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
    post:
      tags:
        - Knowledge
      summary: Create a knowledge document
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
              properties:
                title:
                  type: string
                body:
                  type: string
                tags:
                  type: array
                  items:
                    type: string
      responses:
        "201":
          description: Created
  /api/knowledge/documents/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Knowledge
      summary: Get a knowledge document
      responses:
        "200":
          description: Document
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  document:
                    $ref: "#/components/schemas/KnowledgeDocumentDetail"
        "404":
          description: Not found
    patch:
      tags:
        - Knowledge
      summary: Update a knowledge document
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                body:
                  type: string
                tags:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  document:
                    $ref: "#/components/schemas/KnowledgeDocumentDetail"
        "404":
          description: Not found
        "409":
          description: Optimistic-lock conflict
    delete:
      tags:
        - Knowledge
      summary: Delete a knowledge document (moves to trash)
      responses:
        "200":
          description: Deleted
  /api/knowledge/decisions:
    get:
      tags:
        - Knowledge
      summary: List decisions (ADR-style)
      responses:
        "200":
          description: Decisions
    post:
      tags:
        - Knowledge
      summary: Record a decision
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
              properties:
                title:
                  type: string
                refCode:
                  type: string
                status:
                  type: string
                  enum:
                    - active
                    - proposed
                    - superseded
                decidedAt:
                  type: string
                context:
                  type: string
                whatWeChose:
                  type: string
                tradeoffs:
                  type: array
                  items:
                    type: string
      responses:
        "201":
          description: Created
  /api/knowledge/decisions/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Knowledge
      responses:
        "200":
          description: Decision
    patch:
      tags:
        - Knowledge
      summary: Update a decision
      responses:
        "200":
          description: Updated
    delete:
      tags:
        - Knowledge
      summary: Delete a decision
      responses:
        "200":
          description: Deleted
  /api/knowledge/glossary:
    get:
      tags:
        - Knowledge
      summary: List glossary terms
      responses:
        "200":
          description: Terms
    post:
      tags:
        - Knowledge
      summary: Create a glossary term
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - term
              properties:
                term:
                  type: string
                domain:
                  type: string
                  enum:
                    - product
                    - tech
                    - process
                    - people
                definition:
                  type: string
                aliases:
                  type: array
                  items:
                    type: string
      responses:
        "201":
          description: Created
  /api/knowledge/glossary/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Knowledge
      summary: Get a glossary term
      responses:
        "200":
          description: Glossary term
    patch:
      tags:
        - Knowledge
      summary: Update a glossary term
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Updated
    delete:
      tags:
        - Knowledge
      summary: Delete a glossary term
      responses:
        "200":
          description: Deleted
  /api/knowledge/retros:
    get:
      tags:
        - Knowledge
      summary: List sprint retros
      responses:
        "200":
          description: Retros
    post:
      tags:
        - Knowledge
      summary: Create a sprint retro
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sprintLabel
              properties:
                sprintLabel:
                  type: string
                status:
                  type: string
                  enum:
                    - draft
                    - ready
                    - shared
                windowStart:
                  type: string
                windowEnd:
                  type: string
                wins:
                  type: array
                  items:
                    type: object
                misses:
                  type: array
                  items:
                    type: object
                surprises:
                  type: array
                  items:
                    type: object
      responses:
        "201":
          description: Created
  /api/knowledge/retros/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Knowledge
      summary: Get a sprint retro
      responses:
        "200":
          description: Retro
    patch:
      tags:
        - Knowledge
      summary: Update a sprint retro
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Updated
    delete:
      tags:
        - Knowledge
      responses:
        "200":
          description: Deleted
  /api/knowledge/onboarding:
    get:
      tags:
        - Knowledge
      summary: List onboarding docs
      responses:
        "200":
          description: Onboarding docs
    post:
      tags:
        - Knowledge
      summary: Create or replace an onboarding doc for a role
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - role
              properties:
                role:
                  type: string
                persona:
                  type: string
                earlyWins:
                  type: array
                  items:
                    type: string
                thingsThatMatter:
                  type: array
                  items:
                    type: string
      responses:
        "201":
          description: Created
  /api/knowledge/onboarding/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Knowledge
      summary: Get an onboarding doc
      responses:
        "200":
          description: Onboarding doc
    patch:
      tags:
        - Knowledge
      summary: Update an onboarding doc
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Updated
    delete:
      tags:
        - Knowledge
      responses:
        "200":
          description: Deleted
  /api/calendar/export:
    get:
      tags:
        - Calendar
      summary: Export the workspace calendar as iCalendar
      description: Returns a `text/calendar` .ics attachment covering the full meeting history (paged internally). Requires the `export` capability.
      responses:
        "200":
          description: iCalendar file
          content:
            text/calendar:
              schema:
                type: string
        "401":
          description: Unauthorized
        "403":
          description: Missing export permission
        "404":
          description: No workspace found
  /api/calendar/tasks:
    get:
      tags:
        - Calendar
      summary: List visible tasks scheduled in a date window (inclusive)
      parameters:
        - name: from
          in: query
          required: true
          schema:
            type: string
            pattern: ^\d{4}-\d{2}-\d{2}$
        - name: to
          in: query
          required: true
          schema:
            type: string
            pattern: ^\d{4}-\d{2}-\d{2}$
      responses:
        "200":
          description: Calendar tasks
        "400":
          description: Invalid date window
  /api/support/tickets:
    get:
      tags:
        - Support
      summary: List support tickets
      description: A non-admin asking for scope=workspace is answered with their own tickets and scope=mine in the body — read `scope` to know which list you got.
      parameters:
        - name: scope
          in: query
          description: "'mine' (default) or 'workspace' (admin/owner only)"
          schema:
            type: string
            enum:
              - mine
              - workspace
        - name: category
          in: query
          description: Only tickets of this category (either scope).
          schema:
            type: string
            enum:
              - bug
              - feature
              - question
              - billing
              - general
        - name: limit
          in: query
          description: Max rows (default 50 for mine, 100 for workspace; max 200)
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - name: before
          in: query
          description: Keyset cursor (the previous page's nextCursor) — workspace scope only.
          schema:
            type: string
      responses:
        "200":
          description: Tickets
    post:
      tags:
        - Support
      summary: File a support ticket
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - subject
                - description
              properties:
                subject:
                  type: string
                description:
                  type: string
                category:
                  type: string
                  enum:
                    - bug
                    - feature
                    - question
                    - billing
                    - general
                priority:
                  type: string
                  enum:
                    - low
                    - medium
                    - high
                    - urgent
                diagnostics:
                  type: object
                bug:
                  type: object
                  description: Optional structured half of a bug report (category bug only). Folded into the description server-side; non-string values are ignored.
                  properties:
                    reproductionSteps:
                      type: string
                      maxLength: 4000
                    expectedBehavior:
                      type: string
                      maxLength: 2000
                    actualBehavior:
                      type: string
                      maxLength: 2000
      responses:
        "201":
          description: Ticket created
  /api/support/tickets/{ticketId}:
    parameters:
      - name: ticketId
        in: path
        required: true
        schema:
          type: integer
    patch:
      tags:
        - Support
      summary: Update a ticket's status and/or resolution note (admin/owner only)
      description: When the status moves to in-progress, resolved or closed, or a new note is written, the reporter (if not the caller) is emailed and pushed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: At least one of status or resolutionNote is required.
              properties:
                status:
                  type: string
                  enum:
                    - open
                    - in-progress
                    - resolved
                    - closed
                resolutionNote:
                  type: string
                  nullable: true
                  maxLength: 4000
                  description: What was done to resolve the ticket — shown to the reporter. Pass null to clear.
      responses:
        "200":
          description: Updated
        "403":
          description: Forbidden
        "404":
          description: Not found
  /api/recurring-tasks:
    get:
      tags:
        - RecurringTasks
      summary: List recurring-task generators for a project
      parameters:
        - name: projectRef
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Recurring tasks
    post:
      tags:
        - RecurringTasks
      summary: Create a recurring-task generator
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - projectRef
                - title
                - recurrenceRule
              properties:
                projectRef:
                  type: string
                templateTaskId:
                  type: integer
                title:
                  type: string
                description:
                  type: string
                assignedTo:
                  type: string
                priority:
                  type: string
                tag:
                  type: string
                recurrenceRule:
                  type: string
                  description: iCal RRULE string
                nextDue:
                  type: string
                  format: date
                active:
                  type: boolean
                stopWhenTaskCompleted:
                  type: integer
                  description: Task id whose completion ends the rule — the generator deactivates once that task is completed or deleted. Must be an open task in the workspace.
      responses:
        "201":
          description: Created
  /api/recurring-tasks/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
    put:
      tags:
        - RecurringTasks
      summary: Update a recurring-task generator
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                description:
                  type: string
                assignedTo:
                  type: string
                priority:
                  type: string
                tag:
                  type: string
                recurrenceRule:
                  type: string
                nextDue:
                  type: string
                  format: date
                active:
                  type: boolean
                stopWhenTaskCompleted:
                  type:
                    - integer
                    - "null"
                  description: Task id whose completion ends the rule; null clears the condition.
      responses:
        "200":
          description: Updated
        "404":
          description: Not found
    delete:
      tags:
        - RecurringTasks
      summary: Delete a recurring-task generator
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/sla-policies:
    get:
      tags:
        - SLA
      summary: List SLA policies
      parameters:
        - name: projectRef
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Policies
    post:
      tags:
        - SLA
      summary: Create an SLA policy
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - projectRef
                - name
              properties:
                projectRef:
                  type: string
                name:
                  type: string
                priority:
                  type: string
                responseTimeMinutes:
                  type: integer
                  minimum: 1
                resolutionTimeMinutes:
                  type: integer
                  minimum: 1
                enabled:
                  type: boolean
      responses:
        "201":
          description: Created
  /api/sla-policies/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
    put:
      tags:
        - SLA
      summary: Update an SLA policy
      responses:
        "200":
          description: Updated
        "404":
          description: Not found
    delete:
      tags:
        - SLA
      summary: Delete an SLA policy
      responses:
        "200":
          description: Deleted
  /api/escalation-rules:
    get:
      tags:
        - Escalation
      summary: List escalation rules
      parameters:
        - name: projectRef
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Rules
    post:
      tags:
        - Escalation
      summary: Create an escalation rule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - projectRef
                - name
                - conditionType
                - actionType
              properties:
                projectRef:
                  type: string
                name:
                  type: string
                conditionType:
                  type: string
                conditionConfig:
                  type: object
                actionType:
                  type: string
                actionConfig:
                  type: object
                enabled:
                  type: boolean
      responses:
        "201":
          description: Created
  /api/escalation-rules/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
    put:
      tags:
        - Escalation
      summary: Update an escalation rule
      responses:
        "200":
          description: Updated
        "404":
          description: Not found
    delete:
      tags:
        - Escalation
      summary: Delete an escalation rule
      responses:
        "200":
          description: Deleted
  /api/automations:
    get:
      tags:
        - Automations
      summary: List automation rules
      parameters:
        - name: projectRef
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Automations
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  automations:
                    type: array
                    items:
                      type: object
    post:
      tags:
        - Automations
      summary: Create an automation rule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - projectRef
                - name
                - triggerType
                - actionType
              properties:
                projectRef:
                  type: string
                name:
                  type: string
                triggerType:
                  type: string
                  description: |
                    A trigger from GET /api/automations/vocabulary — task lifecycle (created, moved,
                    completed, assigned, due-date, property-changed), an external event (email-received,
                    integration-item, github-pull-request, github-issue, github-push, calendar-event,
                    finance-event, document-event), or a deal event (deal-stage-changed,
                    deal-document-signed, meeting-booked, bd-match-found). bd-match-found fires once per
                    new deal/partner broker-dealer pairing from the hourly scan; triggerConfig accepts
                    { min_score (a floor, 50-100), stage_kind, pipeline_id }. The client-facing deal
                    actions skip it — pair it with invoke-agent to email the partner.
                triggerConfig:
                  type: object
                actionType:
                  type: string
                  description: |
                    An action from GET /api/automations/vocabulary — a task action (assign, change-status,
                    set-field, add-tag, remove-tag, add-comment, move-to-project, send-notification,
                    set-property), invoke-agent, start-water-cooler, a deal action (provision-client-space,
                    send-engagement-envelope, enroll-cadence, start-investor-campaign — admin-only),
                    start-issuer-campaign (client prospecting on a named issuer campaign; admin-only, task
                    or deal triggers only), or an internal deal-side action (move-deal-stage, create-task).
                actionConfig:
                  type: object
                enabled:
                  type: boolean
      responses:
        "201":
          description: Created
  /api/automations/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
    put:
      tags:
        - Automations
      summary: Update an automation rule
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                triggerType:
                  type: string
                triggerConfig:
                  type: object
                actionType:
                  type: string
                actionConfig:
                  type: object
                enabled:
                  type: boolean
      responses:
        "200":
          description: Updated
        "404":
          description: Not found
    delete:
      tags:
        - Automations
      summary: Delete an automation rule
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/automations/{id}/authorize:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Automations
      summary: Authorize (or revoke) one agent rule to run at Full autonomy
      description: |
        Workspace owner/admin only. Authorizes ONE invoke-agent rule to run its
        agent at Full autonomy above the workspace's autonomy ceiling, without
        raising that ceiling for every member — or revokes it. Each send still
        follows the rule's send_mode ('approval' parks every send for a human).
        Any later change to the rule's trigger or action clears the
        authorization automatically, and it only counts while the authorizing
        admin is still an active owner/admin.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
              properties:
                action:
                  type: string
                  enum:
                    - authorize
                    - revoke
      responses:
        "200":
          description: The rule's authorization after the change
        "400":
          description: Invalid action
          or the rule is not an invoke-agent rule: null
        "403":
          description: Not a workspace owner/admin
        "404":
          description: Not found
  /api/automations/{id}/runs:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Automations
      summary: Recent execution history for one automation rule
      description: |
        Each row is one firing of the rule. Status is success, skipped, or error;
        detail is the action handler's diagnostic string.
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: Runs
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  runs:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        automation_id:
                          type: string
                        workspace_id:
                          type: string
                          format: uuid
                        task_id:
                          type: integer
                          nullable: true
                        trigger_type:
                          type: string
                        status:
                          type: string
                          enum:
                            - success
                            - error
                            - skipped
                        detail:
                          type: string
                          nullable: true
                        ran_at:
                          type: string
                          format: date-time
  /api/projects/{id}/swimlanes:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Project ref (numeric id as string or slug).
    get:
      tags:
        - Swimlanes
      summary: List swimlanes on a project
      parameters:
        - name: includeArchived
          in: query
          description: Pass "1" to include archived lanes.
          schema:
            type: string
      responses:
        "200":
          description: Swimlanes
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  swimlanes:
                    type: array
                    items:
                      $ref: "#/components/schemas/Swimlane"
    post:
      tags:
        - Swimlanes
      summary: Create a swimlane on a project
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                position:
                  type: integer
                  minimum: 0
                  maximum: 9999
                  description: 0-indexed lane position. Omit to append.
                color:
                  type: string
                  nullable: true
                  maxLength: 32
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  swimlane:
                    $ref: "#/components/schemas/Swimlane"
  /api/swimlanes/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Swimlanes
      summary: Update a swimlane (name, position, color, archived)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                position:
                  type: integer
                  minimum: 0
                  maximum: 9999
                color:
                  type: string
                  nullable: true
                  maxLength: 32
                archived:
                  type: boolean
                  description: true archives the swimlane (sets archived_at), false restores it.
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  swimlane:
                    $ref: "#/components/schemas/Swimlane"
        "404":
          description: Not found or no changes
    delete:
      tags:
        - Swimlanes
      summary: Permanently delete a swimlane
      description: Tasks are not deleted, only the lane assignment is removed.
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/projects/{id}/column-limits:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Project ref.
    get:
      tags:
        - Board
      summary: List per-column WIP limits and current open-task counts
      responses:
        "200":
          description: Limits and counts
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  limits:
                    type: array
                    items:
                      $ref: "#/components/schemas/ColumnLimit"
                  counts:
                    type: object
                    additionalProperties:
                      type: integer
                    description: Map of statusId → open task count.
    put:
      tags:
        - Board
      summary: Upsert a WIP limit and/or column-name override for one status
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - statusId
              properties:
                statusId:
                  type: string
                wipLimit:
                  type: integer
                  nullable: true
                  minimum: 1
                  maximum: 1000
                  description: null clears the cap but keeps the row (for nameOverride).
                nameOverride:
                  type: string
                  nullable: true
                  maxLength: 64
      responses:
        "200":
          description: Upserted
    delete:
      tags:
        - Board
      summary: Remove the WIP limit and name override for one status
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - statusId
              properties:
                statusId:
                  type: string
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/projects/{id}/cycle-time:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Project ref.
    get:
      tags:
        - Reports
      summary: Per-status cycle-time stats for a project
      description: |
        Mean, median (p50), and p90 time tasks spent in each column over the last
        sinceDays days. Sourced from app_task_transitions (real column moves).
      parameters:
        - name: sinceDays
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 365
            default: 90
      responses:
        "200":
          description: Cycle-time rows
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  sinceDays:
                    type: integer
                  timings:
                    type: array
                    items:
                      $ref: "#/components/schemas/StatusTiming"
  /api/tasks/{id}/transitions:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
    get:
      tags:
        - Tasks
      summary: Move history for one task
      description: |
        Every column the task has lived in, who moved it, when, and how long it
        sat in the previous status. Append-only; never updated.
      responses:
        "200":
          description: Transitions
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  transitions:
                    type: array
                    items:
                      $ref: "#/components/schemas/TaskTransition"
        "404":
          description: Task not found
  /api/integrations:
    get:
      tags:
        - Integrations
      summary: List available integration providers and the user's connections
      responses:
        "200":
          description: Catalog + connection summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  providers:
                    type: array
                    items:
                      $ref: "#/components/schemas/IntegrationProvider"
                  connections:
                    type: array
                    items:
                      $ref: "#/components/schemas/IntegrationConnection"
  /api/integrations/{provider}:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
        description: Provider slug (e.g. `angelsend`, `gmail`, `google-drive`, `google-calendar`, `slack`, `asana`).
    get:
      tags:
        - Integrations
      summary: Get a single provider plus the caller's connection (if any)
      responses:
        "200":
          description: Provider + connection
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  provider:
                    $ref: "#/components/schemas/IntegrationProvider"
                  connection:
                    oneOf:
                      - $ref: "#/components/schemas/IntegrationConnection"
                      - type: "null"
        "404":
          description: Unknown provider
    put:
      tags:
        - Integrations
      summary: Store an API-key credential for an api_key provider
      description: |
        Only valid when the provider's `authType` is `api_key` (currently
        AngelSend). For OAuth providers use `/api/integrations/{provider}/oauth/start`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - apiKey
              properties:
                apiKey:
                  type: string
                  format: password
                metadata:
                  type: object
                  additionalProperties: true
                  description: Optional caller-supplied display metadata to merge alongside derived metadata.
      responses:
        "200":
          description: Connection saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  connection:
                    $ref: "#/components/schemas/IntegrationConnection"
        "400":
          description: Missing apiKey or wrong auth type
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Unknown provider
    delete:
      tags:
        - Integrations
      summary: Disconnect the caller from a provider
      responses:
        "200":
          description: Removed (`removed` is false if there was no row to begin with)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  removed:
                    type: boolean
  /api/integrations/{provider}/test:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Integrations
      summary: Probe the stored credential against the provider's identity endpoint
      description: |
        Calls the provider's "who am I" endpoint (AngelSend connection-status,
        Gmail profile, Drive about, Slack auth.test, Asana /users/me, Google
        Calendar calendarList) and updates the connection's status + metadata
        based on the result.
      responses:
        "200":
          description: Connection healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  result:
                    description: Raw provider response (shape varies by provider).
        "412":
          description: Not connected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "501":
          description: No test implemented for this provider
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: Provider rejected the credential
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/integrations/{provider}/oauth/start:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Integrations
      summary: Begin an OAuth2 authorization-code flow
      description: |
        Generates a state token, sets it on an httpOnly cookie, then redirects
        the browser to the provider's authorize URL. Only valid for providers
        with `authType: oauth2`.
      responses:
        "302":
          description: Redirect to provider authorize URL
        "400":
          description: Provider does not use OAuth
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: OAuth client credentials not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/integrations/{provider}/oauth/callback:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
      - name: code
        in: query
        schema:
          type: string
      - name: state
        in: query
        schema:
          type: string
      - name: error
        in: query
        schema:
          type: string
      - name: error_description
        in: query
        schema:
          type: string
    get:
      tags:
        - Integrations
      summary: OAuth2 callback (browser-only, exchanges code for tokens)
      description: |
        Verifies the state cookie, exchanges the code for an access/refresh
        token bundle, persists the encrypted bundle to the user's connection,
        then redirects back to `/settings?tab=integrations&oauth=<slug>&status=...`.
      responses:
        "302":
          description: Redirect back to Settings → Integrations with status flash
  /api/integrations/angelsend/calendar:
    get:
      tags:
        - Integrations
      summary: List events from the connected AngelSend calendar
      parameters:
        - name: from
          in: query
          schema:
            type: string
            format: date-time
          description: ISO timestamp window start (optional).
        - name: to
          in: query
          schema:
            type: string
            format: date-time
          description: ISO timestamp window end (optional).
      responses:
        "200":
          description: Events
        "412":
          description: AngelSend not connected
    post:
      tags:
        - Integrations
      summary: Create an event on the connected AngelSend calendar
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - start
                - end
              properties:
                title:
                  type: string
                summary:
                  type: string
                  description: Falls back to `title` if omitted.
                description:
                  type: string
                  nullable: true
                start:
                  type: string
                  format: date-time
                end:
                  type: string
                  format: date-time
                location:
                  type: string
                  nullable: true
                attendees:
                  type: array
                  items:
                    type: object
                    properties:
                      email:
                        type: string
                      name:
                        type: string
      responses:
        "200":
          description: Event created
        "400":
          description: Missing required fields
        "412":
          description: AngelSend not connected
  /api/integrations/angelsend/contacts:
    get:
      tags:
        - Integrations
      summary: List leads / contacts from AngelSend
      description: |
        Newest first, at most 500. AngelSend's list-leads has NO offset or
        cursor, so there is no paging parameter here; `hasMore` says whether
        the list was cut off at `limit`. Page through a larger audience with a
        segment (the Deal Room audience sync pages segment membership).
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            default: 100
            maximum: 500
        - in: query
          name: search
          schema:
            type: string
          description: Matches first name, last name, email or company.
      responses:
        "200":
          description: Contacts
        "412":
          description: AngelSend not connected
  /api/integrations/angelsend/credits:
    get:
      tags:
        - Integrations
      summary: Get the AngelSend credit balance for the connected account
      responses:
        "200":
          description: Balance
        "412":
          description: AngelSend not connected
  /api/integrations/angelsend/actions:
    get:
      tags:
        - Integrations
      summary: Search the AngelSend action catalogue
      description: |
        Browse or search the 81 AngelSend `api-gateway` actions Kavanah wires, each with its parameter schema, scope, and whether it is destructive or spends AngelSend AI credits. Pure catalogue data — makes no call to AngelSend, so it works without a connection.
      parameters:
        - in: query
          name: q
          schema:
            type: string
          description: Free-text search over action names, summaries, and parameter names. Omit to list all.
        - in: query
          name: group
          schema:
            type: string
            enum:
              - crm
              - contacts
              - inbox
              - compliance
              - intent
              - intelligence
              - quality
              - campaigns
              - knowledge
              - chatbots
              - platform
          description: Restrict to one capability group.
        - in: query
          name: limit
          schema:
            type: integer
          description: Max actions to return. Defaults to all.
      responses:
        "200":
          description: Matching actions with their schemas
        "400":
          description: Unknown group
  /api/integrations/angelsend/actions/{action}:
    post:
      tags:
        - Integrations
      summary: Execute one AngelSend api-gateway action
      description: |
        Run a single catalogued AngelSend action. The request body is the action's parameter object — see `GET /api/integrations/angelsend/actions` for each action's schema. Only the 81 catalogued actions are accepted, and parameters are validated (including rejection of unknown keys) before the call is made. For the multiplexed actions (`manage-*`, `configure-follow-up-rule`) the sub-operation goes in the body's `action` field.
      parameters:
        - in: path
          name: action
          required: true
          schema:
            type: string
          description: Action name, e.g. `update-deal`, `get-contact`, `manage-block-list`.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              description: The action's parameters.
      responses:
        "200":
          description: Action result, with the resolved scope and whether it was destructive
        "400":
          description: Invalid or missing parameters
        "404":
          description: Unknown action
        "412":
          description: AngelSend not connected
  /api/integrations/angelsend/email:
    post:
      tags:
        - Integrations
      summary: Send email via AngelSend
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - to
                - subject
              properties:
                to:
                  oneOf:
                    - type: string
                    - type: array
                      items:
                        type: string
                subject:
                  type: string
                body:
                  type: string
                html:
                  type: string
                from:
                  type: string
                replyTo:
                  type: string
                cc:
                  type: array
                  items:
                    type: string
                bcc:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Sent
        "412":
          description: AngelSend not connected
  /api/integrations/gmail/profile:
    get:
      tags:
        - Integrations
      summary: Get the connected Gmail account's profile (email, message counts)
      responses:
        "200":
          description: Profile
        "412":
          description: Gmail not connected
  /api/integrations/gmail/messages:
    get:
      tags:
        - Integrations
      summary: List Gmail message ids (and threadIds)
      parameters:
        - name: q
          in: query
          schema:
            type: string
          description: Gmail search query syntax.
        - name: maxResults
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: pageToken
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Messages
        "412":
          description: Gmail not connected
    post:
      tags:
        - Integrations
      summary: Send a plain-text email through Gmail
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - to
              properties:
                to:
                  type: string
                subject:
                  type: string
                body:
                  type: string
                cc:
                  type: string
                bcc:
                  type: string
                replyTo:
                  type: string
      responses:
        "200":
          description: Sent
        "400":
          description: Missing to / subject / body
        "412":
          description: Gmail not connected
  /api/integrations/google-drive/about:
    get:
      tags:
        - Integrations
      summary: Get the Drive account's identity and storage quota
      responses:
        "200":
          description: About
        "412":
          description: Google Drive not connected
  /api/integrations/google-drive/files:
    get:
      tags:
        - Integrations
      summary: List Drive files visible to the app (drive.file scope)
      parameters:
        - name: q
          in: query
          schema:
            type: string
          description: Drive query syntax.
        - name: pageSize
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: pageToken
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Files
        "412":
          description: Google Drive not connected
    post:
      tags:
        - Integrations
      summary: Create a UTF-8 text file in Drive
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                content:
                  type: string
                  description: UTF-8 file body. Defaults to empty string.
                mimeType:
                  type: string
                  description: Defaults to `text/plain; charset=UTF-8`.
                parentFolderId:
                  type: string
      responses:
        "200":
          description: File
        "400":
          description: Missing name
        "412":
          description: Google Drive not connected
  /api/integrations/google-calendar/calendars:
    get:
      tags:
        - Integrations
      summary: List the user's Google calendars
      responses:
        "200":
          description: Calendars
        "412":
          description: Google Calendar not connected
  /api/integrations/google-calendar/events:
    get:
      tags:
        - Integrations
      summary: List events from one of the user's Google calendars
      parameters:
        - name: calendarId
          in: query
          schema:
            type: string
          description: Defaults to `primary`.
        - name: timeMin
          in: query
          schema:
            type: string
            format: date-time
          description: Window start. Also accepts `from`.
        - name: timeMax
          in: query
          schema:
            type: string
            format: date-time
          description: Window end. Also accepts `to`.
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
        - name: maxResults
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 2500
        - name: pageToken
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Events
        "412":
          description: Google Calendar not connected
    post:
      tags:
        - Integrations
      summary: Create an event on one of the user's Google calendars
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - summary
                - start
                - end
              properties:
                calendarId:
                  type: string
                  description: Defaults to `primary`.
                summary:
                  type: string
                description:
                  type: string
                location:
                  type: string
                start:
                  type: string
                  format: date-time
                end:
                  type: string
                  format: date-time
                timeZone:
                  type: string
                attendees:
                  type: array
                  items:
                    type: string
                sendUpdates:
                  type: string
                  enum:
                    - all
                    - externalOnly
                    - none
      responses:
        "200":
          description: Event created
        "400":
          description: Missing summary / start / end
        "412":
          description: Google Calendar not connected
  /api/integrations/slack/auth-test:
    get:
      tags:
        - Integrations
      summary: Run Slack's auth.test method (returns bot/user/team identity)
      responses:
        "200":
          description: Identity
        "412":
          description: Slack not connected
  /api/integrations/slack/channels:
    get:
      tags:
        - Integrations
      summary: List channels visible to the connected Slack bot
      parameters:
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - name: types
          in: query
          schema:
            type: string
          description: Slack `types` filter, e.g. `public_channel,private_channel`.
        - name: excludeArchived
          in: query
          schema:
            type: boolean
      responses:
        "200":
          description: Channels
        "412":
          description: Slack not connected
  /api/integrations/slack/messages:
    post:
      tags:
        - Integrations
      summary: Post a message to a Slack channel via chat.postMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - channel
                - text
              properties:
                channel:
                  type: string
                  description: Channel id (C…) or `#name`.
                text:
                  type: string
                threadTs:
                  type: string
      responses:
        "200":
          description: Posted
        "400":
          description: Missing channel / text
        "412":
          description: Slack not connected
    patch:
      tags:
        - Integrations
      summary: Edit a Slack message via chat.update
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - channel
                - ts
                - text
              properties:
                channel:
                  type: string
                ts:
                  type: string
                  description: Timestamp id of the message to edit.
                text:
                  type: string
      responses:
        "200":
          description: Updated
        "400":
          description: Missing channel / ts / text
    delete:
      tags:
        - Integrations
      summary: Delete a Slack message via chat.delete
      description: Accepts channel and ts either as query params or in a JSON body.
      parameters:
        - name: channel
          in: query
          schema:
            type: string
        - name: ts
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Deleted
        "400":
          description: Missing channel / ts
  /api/integrations/asana/users-me:
    get:
      tags:
        - Integrations
      summary: Get the connected Asana user's profile (name, email, workspaces)
      responses:
        "200":
          description: User
        "412":
          description: Asana not connected
  /api/integrations/asana/workspaces:
    get:
      tags:
        - Integrations
      summary: List Asana workspaces the connected account belongs to
      responses:
        "200":
          description: Workspaces
        "412":
          description: Asana not connected
  /api/integrations/slack/workspaces:
    get:
      tags:
        - Integrations
      summary: List the Slack workspaces the caller has connected, the default first
      responses:
        "200":
          description: Workspaces
  /api/integrations/slack/workspaces/{teamId}:
    delete:
      tags:
        - Integrations
      summary: Remove one connected Slack workspace (not the default — Disconnect removes that)
      parameters:
        - name: teamId
          in: path
          required: true
          schema:
            type: string
          description: Slack team id (T…).
      responses:
        "200":
          description: Removed
        "404":
          description: That workspace is not connected
        "409":
          description: It is the default workspace
  /api/integrations/asana/tasks:
    get:
      tags:
        - Integrations
      summary: List Asana tasks (filtered by workspace or project)
      parameters:
        - name: workspace
          in: query
          schema:
            type: string
          description: Workspace gid (required unless project is given).
        - name: project
          in: query
          schema:
            type: string
          description: Project gid.
        - name: assignee
          in: query
          schema:
            type: string
          description: User gid or `me`.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
      responses:
        "200":
          description: Tasks
        "400":
          description: Neither workspace nor project provided
        "412":
          description: Asana not connected
    post:
      tags:
        - Integrations
      summary: Create a task in an Asana workspace
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - workspace
                - name
              properties:
                workspace:
                  type: string
                name:
                  type: string
                notes:
                  type: string
                assignee:
                  type: string
                dueOn:
                  type: string
                  format: date
                projects:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Task
        "400":
          description: Missing workspace or name
        "412":
          description: Asana not connected
  /api/ai-agent:
    post:
      tags:
        - AI Agent
      summary: Send a chat message to the AI agent
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/ai-agent/attachments:
    delete:
      tags:
        - AI Agent
      summary: Delete attachments
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - AI Agent
      summary: List attachments
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - AI Agent
      summary: Create attachment
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/ai-agent/audit:
    get:
      tags:
        - AI Agent
      summary: List audit
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/ai-agent/config:
    get:
      tags:
        - AI Agent
      summary: List config
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - AI Agent
      summary: Update config
      description: |
        Partial update of the workspace agent config. `background_model` sets the default model for unattended runs (schedules, task runs, exec meetings): a model id from `model_options`, `"auto"` (a small router model grades each run and picks the cheapest model that fits), or null to use `model`. `auto_optimize_for` (`cost` | `performance` | `speed`, null = cost) sets what auto mode optimizes for, in chat auto mode and unattended routing.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/ai-agent/conversations:
    get:
      tags:
        - AI Agent
      summary: List conversations
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - AI Agent
      summary: Create conversation
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/ai-agent/conversations/search:
    get:
      tags:
        - AI Agent
      summary: Search conversations
      description: Full-text search over the caller's own past agent conversations (message bodies and titles). Scoped to the authenticated (workspace, user).
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
            maxLength: 300
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 20
            maximum: 25
      responses:
        "200":
          description: Success
        "400":
          description: Query too long
        "401":
          description: Unauthorized
        "403":
          description: AI agent disabled for this workspace
  /api/ai-agent/conversations/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - AI Agent
      summary: Delete conversation
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - AI Agent
      summary: Fetch conversation
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - AI Agent
      summary: Update conversation
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/ai-agent/conversations/{id}/export:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - AI Agent
      summary: List export
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/ai-agent/custom-tools:
    get:
      tags:
        - AI Agent
      summary: List custom tools
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - AI Agent
      summary: Create custom tool
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/ai-agent/custom-tools/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - AI Agent
      summary: Delete custom tool
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - AI Agent
      summary: Update custom tool
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/ai-agent/feedback:
    delete:
      tags:
        - AI Agent
      summary: Delete feedback
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - AI Agent
      summary: Create feedback
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/ai-agent/feedback/analytics:
    get:
      tags:
        - AI Agent
      summary: List analytics
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/ai-agent/jobs:
    get:
      tags:
        - AI Agent
      summary: List jobs
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/ai-agent/jobs/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - AI Agent
      summary: Fetch job
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/ai-agent/mention:
    post:
      tags:
        - AI Agent
      summary: Ask the AI agent about context you supply
      description: |
        Stateless: returns the answer and stores nothing. To answer an @agent question in a stored task comment or discussion post — and save the answer as the agent — use /api/tasks/{id}/comments/agent-reply or /api/discussions/agent-reply instead. Failures are non-2xx with a `reason`, never a 200 carrying an apology as the answer.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                content:
                  type: string
                  maxLength: 4000
                  description: The comment text, including the @agent mention.
                context:
                  type: string
                  maxLength: 16000
                contextType:
                  type: string
                  enum:
                    - task-comment
                    - discussion-reply
      responses:
        "200":
          description: The agent's answer
          content:
            application/json:
              schema:
                type: object
                properties:
                  reply:
                    type: string
                  truncated:
                    type: boolean
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "403":
          description: The AI agent is disabled for this workspace
        "429":
          description: Rate limited
          or the daily AI token budget is spent: null
        "502":
          description: The model could not answer
        "503":
          description: The AI agent is not configured
  /api/ai-agent/preferences:
    get:
      tags:
        - AI Agent
      summary: List preferences
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - AI Agent
      summary: Update preferences
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/ai-agent/tools:
    get:
      tags:
        - AI Agent
      summary: List the agent tools the caller may execute
      description: |
        Filtered by persona scope, read-only action mode, data scopes and ADMIN_ONLY_TOOLS — this is the caller's effective toolset, not the raw registry.
      parameters:
        - name: persona_id
          in: query
          schema:
            type: string
          description: Evaluate the toolset as this persona.
        - name: q
          in: query
          schema:
            type: string
          description: Case-insensitive filter over name and description.
      responses:
        "200":
          description: Available tools
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  count:
                    type: integer
                  tools:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        description:
                          type: string
                        read_only:
                          type: boolean
                        category:
                          type: string
                          nullable: true
                          description: Action-policy category; null means not policy-gated.
                        source:
                          type: string
                          enum:
                            - builtin
                            - custom
                            - app
                        input_schema:
                          type: object
                          nullable: true
        "403":
          description: The AI agent is not enabled for this workspace
  /api/ai-agent/tools/{name}/execute:
    post:
      tags:
        - AI Agent
      summary: Execute one agent tool by name
      description: |
        Deterministic counterpart to POST /api/ai-agent. Applies the full governance stack: effective-toolset filtering, argument guardrails, the action policy (FAIL CLOSED — an `approve` verdict parks rather than runs, unlike interactive chat), the action ledger and undo recording.
      parameters:
        - name: name
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                input:
                  type: object
                  additionalProperties: true
                  description: Arguments for the tool, per its input_schema.
                persona_id:
                  type: string
                  description: Execute within this persona's scope.
                run_id:
                  type: string
                  description: |
                    Idempotency scope. Reusing it after a timeout returns the first result instead of repeating a risky write.
      responses:
        "200":
          description: Executed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  status:
                    type: string
                    enum:
                      - executed
                  tool:
                    type: string
                  category:
                    type: string
                    nullable: true
                  content:
                    type: string
                  run_id:
                    type: string
                  undo_group:
                    type: object
                    nullable: true
                    properties:
                      id:
                        type: string
                      label:
                        type: string
                      count:
                        type: integer
        "202":
          description: |
            Parked for human approval (policy mode `approve`, or a daily budget exhausted). Nothing ran. See GET /api/ai-agent/approvals.
        "403":
          description: Blocked by a guardrail or by the workspace action policy
        "404":
          description: Unknown tool, or not available to this caller
        "500":
          description: The tool threw, or the approval row could not be recorded
  /api/mcp:
    get:
      tags:
        - AI Agent
      summary: MCP transport probe
      description: |
        The MCP server uses the streamable HTTP transport, which is POST-only — there are no server-initiated messages to stream. GET always returns 405 with the supported protocol versions, rather than opening an SSE stream that never emits and looks to a client like a hang.
      responses:
        "405":
          description: Use POST
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  protocol_versions:
                    type: array
                    items:
                      type: string
    post:
      tags:
        - AI Agent
      summary: Model Context Protocol (MCP) server
      description: |
        JSON-RPC 2.0 endpoint letting external MCP clients (Claude Desktop, IDEs, third-party agents) drive the Kavanah workspace. Authenticate with a workspace API key as a Bearer token; the key inherits the minting user's RBAC.

        Methods: `initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call`. JSON-RPC batching is not supported (removed in MCP 2025-06-18).

        Tool calls run through the SAME governance stack as POST /api/ai-agent/tools/{name}/execute — effective-toolset filtering, argument guardrails, the FAIL-CLOSED action policy, the action ledger and undo recording. A write whose policy is `approve` is PARKED, not run: the result comes back with `isError: true` and an approval id, and nothing has changed. A tool the caller cannot reach is reported as unknown, so the registry is not probeable.

        The tool registry is large, so `tools/list` is cursor-paginated and the set can be narrowed with the query parameters below.
      parameters:
        - name: persona_id
          in: query
          required: false
          schema:
            type: string
          description: |
            Run as this persona, narrowing the toolset to its scopes. A persona the caller cannot access is refused rather than silently detached.
        - name: q
          in: query
          required: false
          schema:
            type: string
          description: Filter tools/list by substring of name or description.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - jsonrpc
                - method
              properties:
                jsonrpc:
                  type: string
                  enum:
                    - "2.0"
                id:
                  oneOf:
                    - type: string
                    - type: integer
                  description: Omit for notifications; the server then returns 202 with no body.
                method:
                  type: string
                  enum:
                    - initialize
                    - notifications/initialized
                    - ping
                    - tools/list
                    - tools/call
                params:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: JSON-RPC response (result or error)
          content:
            application/json:
              schema:
                type: object
                properties:
                  jsonrpc:
                    type: string
                    enum:
                      - "2.0"
                  id:
                    oneOf:
                      - type: string
                      - type: integer
                      - type: "null"
                  result:
                    type: object
                    additionalProperties: true
                  error:
                    type: object
                    properties:
                      code:
                        type: integer
                      message:
                        type: string
        "202":
          description: Notification accepted. No body — JSON-RPC forbids replying to a message with no id.
        "400":
          description: Malformed JSON, a batch request, or an invalid JSON-RPC frame
        "401":
          description: Invalid or revoked API key
  /api/ai-agent/undo:
    post:
      tags:
        - AI Agent
      summary: Create undo
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/scim/logs:
    get:
      tags:
        - Admin
      summary: List logs
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/scim/providers:
    get:
      tags:
        - Admin
      summary: List providers
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Admin
      summary: Create provider
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/scim/providers/{providerId}:
    parameters:
      - name: providerId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Admin
      summary: Delete provider
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Admin
      summary: Fetch provider
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Admin
      summary: Update provider
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/scim/providers/{providerId}/mappings:
    parameters:
      - name: providerId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Admin
      summary: List mappings
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Admin
      summary: Create mapping
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/scim/providers/{providerId}/mappings/{mappingId}:
    parameters:
      - name: providerId
        in: path
        required: true
        schema:
          type: string
      - name: mappingId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Admin
      summary: Delete mapping
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/admin/scim/providers/{providerId}/resync:
    parameters:
      - name: providerId
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Admin
      summary: Create resync
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/scim/providers/{providerId}/tokens:
    parameters:
      - name: providerId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Admin
      summary: List tokens
      description: Bearer tokens for this SCIM provider, newest first. Revoked tokens are included. The plaintext token is never returned — only its hash is stored.
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
        "404":
          description: Not found
    post:
      tags:
        - Admin
      summary: Create token
      description: Mint an additional bearer token for this provider (credential rotation). The plaintext `bearerToken` is returned exactly once and cannot be retrieved again.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                label:
                  type: string
      responses:
        "201":
          description: Created
        "401":
          description: Unauthorized
        "404":
          description: Not found
  /api/admin/scim/providers/{providerId}/tokens/{tokenId}:
    parameters:
      - name: providerId
        in: path
        required: true
        schema:
          type: string
      - name: tokenId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Admin
      summary: Delete token
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/record-shares:
    get:
      tags:
        - Clients
      summary: List record shares
      description: Who an account or deal is shared with, plus the caller's own access level and the pickable members/teams. Answers 404 when the caller cannot reach the record, so it can't be used to enumerate private records.
      parameters:
        - name: resourceType
          in: query
          required: true
          schema:
            type: string
            enum:
              - client
              - deal
        - name: resourceId
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "404":
          description: Not found
    post:
      tags:
        - Clients
      summary: Create record share
      description: Grant or update a share, and/or flip the record's visibility between 'workspace' and 'private'. Restricted to the record owner or a workspace admin — an `edit` share does NOT confer the right to re-share.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - resourceType
                - resourceId
              properties:
                resourceType:
                  type: string
                  enum:
                    - client
                    - deal
                resourceId:
                  type: string
                subjectType:
                  type: string
                  enum:
                    - user
                    - team
                subjectId:
                  type: string
                access:
                  type: string
                  enum:
                    - view
                    - edit
                visibility:
                  type: string
                  enum:
                    - workspace
                    - private
      responses:
        "200":
          description: Visibility updated
        "201":
          description: Share created
        "400":
          description: Invalid request
        "403":
          description: Not the owner or an admin
        "404":
          description: Not found
    delete:
      tags:
        - Clients
      summary: Delete record share
      parameters:
        - name: resourceType
          in: query
          required: true
          schema:
            type: string
            enum:
              - client
              - deal
        - name: resourceId
          in: query
          required: true
          schema:
            type: string
        - name: subjectType
          in: query
          required: true
          schema:
            type: string
            enum:
              - user
              - team
        - name: subjectId
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Removed
        "400":
          description: Invalid request
        "403":
          description: Not the owner or an admin
        "404":
          description: Not found
  /api/admin/security/domains:
    get:
      tags:
        - Admin
      summary: List domains
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Admin
      summary: Create domain
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/security/domains/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Admin
      summary: Delete domain
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/admin/security/ip-allowlist:
    get:
      tags:
        - Admin
      summary: List ip allowlist
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Admin
      summary: Create ip allowlist
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/security/ip-allowlist/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Admin
      summary: Delete ip allowlist
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/admin/security/policies:
    get:
      tags:
        - Admin
      summary: List policies
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Admin
      summary: Create a security policy
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/security/sessions:
    get:
      tags:
        - Admin
      summary: List sessions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/security/sessions/{sessionId}:
    parameters:
      - name: sessionId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Admin
      summary: Delete session
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/admin/security/sso:
    get:
      tags:
        - Admin
      summary: List sso
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Admin
      summary: Create sso
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/super:
    get:
      tags:
        - Admin
      summary: List super
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/compliance-requests:
    get:
      tags:
        - Admin
      summary: List compliance requests
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Admin
      summary: Update compliance requests
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/data-governance:
    get:
      tags:
        - Admin
      summary: List data governance
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Admin
      summary: Update data governance
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/data-governance/export/{userId}:
    parameters:
      - name: userId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Admin
      summary: Fetch export
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/data-governance/pii:
    post:
      tags:
        - Admin
      summary: Create pii
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/super/data-governance/redact:
    post:
      tags:
        - Admin
      summary: Create redact
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/super/data-governance/requests/{requestId}:
    parameters:
      - name: requestId
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Admin
      summary: Update request
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/meta:
    get:
      tags:
        - Admin
      summary: List meta
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/settings:
    get:
      tags:
        - Admin
      summary: List settings
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Admin
      summary: Update settings
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/settings/logo:
    post:
      tags:
        - Admin
      summary: Create logo
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/super/settings/test:
    post:
      tags:
        - Admin
      summary: Create test
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/super/support-inbox:
    get:
      tags:
        - Admin
      summary: List support inbox
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/support-inbox/submissions/{submissionId}:
    parameters:
      - name: submissionId
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Admin
      summary: Update submission
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/support-inbox/tickets/{ticketId}:
    parameters:
      - name: ticketId
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Admin
      summary: Update ticket
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/tooling:
    get:
      tags:
        - Admin
      summary: List tooling
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/tooling/billing:
    patch:
      tags:
        - Admin
      summary: Update billing
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/tooling/bulk-tasks:
    post:
      tags:
        - Admin
      summary: Create bulk task
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/super/tooling/config:
    get:
      tags:
        - Admin
      summary: List config
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Admin
      summary: Create config
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/super/tooling/feature-flags:
    delete:
      tags:
        - Admin
      summary: Delete feature flags
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - Admin
      summary: Create feature flag
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/super/tooling/integrations:
    delete:
      tags:
        - Admin
      summary: Delete integrations
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - Admin
      summary: Create integration
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/super/tooling/quotas:
    patch:
      tags:
        - Admin
      summary: Update quotas
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/tooling/sandboxes:
    delete:
      tags:
        - Admin
      summary: Delete a sandbox
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - Admin
      summary: Create a sandbox
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/admin/super/users/{userId}:
    parameters:
      - name: userId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Admin
      summary: Delete user
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - Admin
      summary: Update user
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/super/workspaces/{workspaceId}:
    parameters:
      - name: workspaceId
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Admin
      summary: Update workspace
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/users/duplicates:
    get:
      tags:
        - Admin
      summary: List duplicates
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/admin/users/merge:
    post:
      tags:
        - Admin
      summary: Create merge
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/agent-alerts:
    get:
      tags:
        - Agent Alerts
      summary: List agent alerts
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Agent Alerts
      summary: Update agent alerts
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/agent-alerts/briefing:
    get:
      tags:
        - Agent Alerts
      summary: List briefing
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/agent-alerts/dismiss:
    get:
      tags:
        - Agent Alerts
      summary: List dismissed agent alerts
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Agent Alerts
      summary: Dismiss an agent alert
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/agent-alerts/preferences:
    get:
      tags:
        - Agent Alerts
      summary: List preferences
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Agent Alerts
      summary: Update preferences
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/agent-alerts/scan:
    post:
      tags:
        - Agent Alerts
      summary: Create scan
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/audit-logs:
    get:
      tags:
        - Audit Logs
      summary: List audit logs
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/audit-logs/alerts:
    delete:
      tags:
        - Audit Logs
      summary: Delete alerts
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Audit Logs
      summary: List alerts
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Audit Logs
      summary: Update alerts
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Audit Logs
      summary: Create alert
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/audit-logs/archiving:
    get:
      tags:
        - Audit Logs
      summary: Get compliance archiving settings and health
      description: The workspace archive (journal) address, compliance mode, retention period, the compliance journal backlog, recent digest deliveries to the archive, and each member's AngelSend archive state. Owners and admins.
      responses:
        "200":
          description: Success
        "403":
          description: Not a workspace owner or admin
    patch:
      tags:
        - Audit Logs
      summary: Update compliance archiving
      description: Set the archive address (`archiveAddress`, one mailbox or null), compliance mode (`enabled`) and retention (`retentionDays`, 365-3650). With compliance mode on, every outbound email carries the archive BCC and cannot skip it, and chat, client-portal, discussion, task-comment, in-call, transcript and agent communications are journaled and delivered to the archive as a daily digest. Workspace owner only; audited.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                archiveAddress:
                  type:
                    - string
                    - "null"
                enabled:
                  type: boolean
                retentionDays:
                  type: integer
                  minimum: 365
                  maximum: 3650
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "403":
          description: Not the workspace owner
  /api/audit-logs/archiving/angelsend-sync:
    post:
      tags:
        - Audit Logs
      summary: Re-sync members' AngelSend archive
      description: Point every member's AngelSend projects at the workspace archive address now (or clear the address Kavanah set when compliance mode is off). Never overwrites an address someone else set. Workspace owner only.
      responses:
        "200":
          description: Success
        "403":
          description: Not the workspace owner
  /api/audit-logs/archiving/deliver:
    post:
      tags:
        - Audit Logs
      summary: Deliver the compliance journal now
      description: Send pending compliance journal entries to the archive address without waiting for the daily digest. Never re-sends delivered entries.
      responses:
        "200":
          description: Success
        "400":
          description: No archive address configured
        "409":
          description: A delivery is already running
        "502":
          description: The archive message could not be sent
  /api/audit-logs/archiving/test:
    post:
      tags:
        - Audit Logs
      summary: Send a test message to the archive address
      responses:
        "200":
          description: Success
        "502":
          description: The test message could not be sent
  /api/audit-logs/compliance:
    get:
      tags:
        - Audit Logs
      summary: List compliance
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/audit-logs/data-access:
    get:
      tags:
        - Audit Logs
      summary: List data access
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/audit-logs/event-config:
    get:
      tags:
        - Audit Logs
      summary: List event config
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Audit Logs
      summary: Create event config
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/audit-logs/export:
    get:
      tags:
        - Audit Logs
      summary: List export
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/audit-logs/retention:
    get:
      tags:
        - Audit Logs
      summary: List retention
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Audit Logs
      summary: Create retention
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/audit-logs/search:
    get:
      tags:
        - Audit Logs
      summary: List search
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/audit-logs/webhooks:
    delete:
      tags:
        - Audit Logs
      summary: Delete webhooks
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Audit Logs
      summary: List webhooks
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Audit Logs
      summary: Update webhooks
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Audit Logs
      summary: Create webhook
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/auth/accounts/list:
    get:
      tags:
        - Auth
      summary: List linked accounts on this device
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/auth/accounts/remove:
    post:
      tags:
        - Auth
      summary: Unlink an account from this device
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/auth/accounts/switch:
    post:
      tags:
        - Auth
      summary: Switch to another linked account
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/auth/github/start:
    get:
      tags:
        - Auth
      summary: List start
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/auth/google/start:
    get:
      tags:
        - Auth
      summary: List start
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/auth/mfa/enroll:
    delete:
      tags:
        - Auth
      summary: Disable two-factor authentication
      description: |
        Requires the account password. With no `scope`, removes every second factor (authenticator, passkeys, recovery codes). With `scope: "authenticator"`, removes only the authenticator app and keeps passkeys — unless it is the last factor, in which case it is a full disable under the same rules. Refused with 403 when the workspace policy mandates MFA and nothing would remain. Audited and emailed to the account holder.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                password:
                  type: string
                scope:
                  type: string
                  enum:
                    - authenticator
              required:
                - password
      responses:
        "200":
          description: Deleted
        "400":
          description: Invalid request
        "401":
          description: Invalid password
        "403":
          description: Workspace policy requires MFA
        "404":
          description: No authenticator app is set up
    get:
      tags:
        - Auth
      summary: Start TOTP enrolment
      description: |
        Returns a QR code and manual-entry secret. Refuses with 409 when a verified authenticator already exists — starting again would replace the working secret. A registered passkey does not block this.
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
        "409":
          description: An authenticator app is already set up
    post:
      tags:
        - Auth
      summary: Finish TOTP enrolment
      description: |
        Verifies a code from the new authenticator. When this is the account's first second factor, returns `recoveryCodes` (shown once). Added beside a passkey, the existing recovery codes are kept and none are returned.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                code:
                  type: string
              required:
                - code
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request or code
        "401":
          description: Unauthorized
  /api/auth/mfa/required:
    get:
      tags:
        - Auth
      summary: Check whether MFA enrolment is still owed
      description: |
        Returns true when the workspace has `mfa_required` on and the caller has no verified second factor. The app shell uses it to redirect to setup; the enforcement itself is in the API auth wrapper, which 403s every other workspace route for the same caller with `code: mfa_setup_required`.
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/auth/mfa/recovery-codes:
    get:
      tags:
        - Auth
      summary: List recovery codes
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Auth
      summary: Regenerate recovery codes
      description: |
        Requires a CURRENT second factor as step-up: `totpCode`, or `webauthn` (an assertion over options from GET /api/auth/mfa/webauthn/step-up). A recovery code is not accepted. Invalidates every previous code.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                totpCode:
                  type: string
                webauthn:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized or invalid second factor
  /api/auth/mfa/verify:
    post:
      tags:
        - Auth
      summary: Complete the second-factor challenge
      description: |
        Takes the short-lived `mfaToken` issued after the first factor (password, Google/GitHub/OneProfile, or SSO) plus ONE of `code`, `webauthn` or `recoveryCode`. On success the sign-in is finished the way its first factor would have finished it — session and refresh cookies, linked-account entry, and `redirectPath`. A sign-in started in the mobile app instead returns `redirectUrl`, a kavanah:// deep link carrying a one-time exchange code.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                mfaToken:
                  type: string
                code:
                  type: string
                webauthn:
                  type: object
                  additionalProperties: true
                recoveryCode:
                  type: string
                nextPath:
                  type: string
                rememberMe:
                  type: boolean
              required:
                - mfaToken
      responses:
        "200":
          description: Signed in
        "400":
          description: Invalid request
        "401":
          description: Invalid or expired token
          or invalid second factor: null
        "429":
          description: Rate limited or account locked
  /api/auth/mfa/webauthn:
    get:
      tags:
        - Auth
      summary: List your passkeys
      description: |
        The caller's own passkeys and security keys — name, added, last used. Never returns key material or credential ids.
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/auth/mfa/webauthn/{credentialId}:
    parameters:
      - name: credentialId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Auth
      summary: Remove a passkey
      description: |
        Requires the account password. When the passkey is the account's last second factor this is a full 2FA disable, refused with 403 if the workspace requires MFA. Audited and emailed.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                password:
                  type: string
              required:
                - password
      responses:
        "200":
          description: Deleted
        "400":
          description: Invalid request
        "401":
          description: Invalid password
        "403":
          description: Workspace policy requires MFA
        "404":
          description: Passkey not found
  /api/auth/mfa/webauthn/authenticate:
    post:
      tags:
        - Auth
      summary: Start a passkey challenge at sign-in
      description: |
        Pre-session: takes the `mfaToken` from the first sign-in step and returns WebAuthn assertion options for that account's passkeys. The assertion is submitted to POST /api/auth/mfa/verify as `webauthn`.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                mfaToken:
                  type: string
              required:
                - mfaToken
      responses:
        "200":
          description: Success
        "401":
          description: Invalid or expired token
        "404":
          description: No passkey registered
        "429":
          description: Rate limited
  /api/auth/mfa/webauthn/register:
    get:
      tags:
        - Auth
      summary: Start registering a passkey
      description: Returns WebAuthn registration options for navigator.credentials.create().
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
        "409":
          description: Passkey limit reached
    post:
      tags:
        - Auth
      summary: Finish registering a passkey
      description: |
        Verifies the attestation and stores the credential. When this is the account's first second factor, returns `recoveryCodes` (shown once).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                response:
                  type: object
                  additionalProperties: true
                name:
                  type: string
              required:
                - response
      responses:
        "200":
          description: Success
        "400":
          description: Could not be verified
        "401":
          description: Unauthorized
        "409":
          description: Already registered
          or limit reached: null
  /api/auth/mfa/webauthn/step-up:
    get:
      tags:
        - Auth
      summary: Start a passkey step-up while signed in
      description: |
        WebAuthn assertion options for re-proving a passkey — used to regenerate recovery codes on a passkey-only account.
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
        "404":
          description: No passkey registered
  /api/auth/oneprofile/start:
    get:
      tags:
        - Auth
      summary: List start
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/auth/refresh:
    post:
      tags:
        - Auth
      summary: Create refresh
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/auth/resend-verification:
    post:
      tags:
        - Auth
      summary: Create resend verification
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/auth/sso/oidc/callback:
    get:
      tags:
        - Auth
      summary: List callback
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/auth/sso/oidc/login:
    post:
      tags:
        - Auth
      summary: Create login
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/auth/sso/saml/acs:
    post:
      tags:
        - Auth
      summary: SAML assertion consumer service (ACS) callback
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/auth/sso/saml/login:
    post:
      tags:
        - Auth
      summary: Create login
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/auth/sso/saml/metadata:
    get:
      tags:
        - Auth
      summary: List metadata
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/auth/verify-email:
    get:
      tags:
        - Auth
      summary: List verify email
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Auth
      summary: Create verify email
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/auth/workspace-selection:
    get:
      tags:
        - Auth
      summary: List workspace selection
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/automations/page-data:
    get:
      tags:
        - Automations
      summary: List page data
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/credits/summary:
    get:
      tags:
        - Billing
      summary: List summary
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/portal/auth/accept-invite:
    post:
      tags:
        - Client Portal
      summary: Create accept invite
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portal/auth/login:
    post:
      tags:
        - Client Portal
      summary: Create login
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portal/auth/logout:
    post:
      tags:
        - Client Portal
      summary: Create logout
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portal/decisions:
    get:
      tags:
        - Client Portal
      summary: List decisions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Client Portal
      summary: Create decision
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portal/discussions:
    post:
      tags:
        - Client Portal
      summary: Create discussion
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portal/export:
    get:
      tags:
        - Client Portal
      summary: List export
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/portal/files:
    post:
      tags:
        - Client Portal
      summary: Create file
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portal/files/{fileId}:
    parameters:
      - name: fileId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Client Portal
      summary: Fetch file
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/clients/{id}/employees:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Clients
      summary: Create employee
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/clients/{id}/employees/{employeeId}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: employeeId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Clients
      summary: Delete employee
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - Clients
      summary: Update employee
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/clients/export:
    get:
      tags:
        - Clients
      summary: List export
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/communication:
    get:
      tags:
        - Communication
      summary: List communication
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Communication
      summary: Create communication
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/components:
    get:
      tags:
        - Components
      summary: List components
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Components
      summary: Create component
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/components/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Components
      summary: Delete component
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    put:
      tags:
        - Components
      summary: Update component
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/cron/ai-agent-jobs:
    get:
      tags:
        - Cron
      summary: List ai agent jobs
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Cron
      summary: Create ai agent job
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/ai-agent-retention:
    post:
      tags:
        - Cron
      summary: Create ai agent retention
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/prospect-research:
    get:
      tags:
        - Cron
      summary: Research contacts due a personalized cadence step (Vercel cron)
      responses:
        "200":
          description: "`{workspaces, researched, factsKept, skipped, errors}`"
        "401":
          description: Unauthorized
    post:
      tags:
        - Cron
      summary: Research contacts due a personalized cadence step
      description: |
        Only in workspaces with BOTH prospect_research_enabled and
        cadence_personalization_enabled. Only contacts whose next cadence step
        is personalized and due within 48 hours, once per contact per 30 days,
        at most 25 per workspace per day. Billed to the workspace owner.
      responses:
        "200":
          description: "`{workspaces, researched, factsKept, skipped, errors}`"
        "401":
          description: Unauthorized
  /api/cron/audit-log-immutable-sink:
    post:
      tags:
        - Cron
      summary: Create audit log immutable sink
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/audit-log-retention:
    post:
      tags:
        - Cron
      summary: Create audit log retention
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/billing-dunning:
    post:
      tags:
        - Cron
      summary: Create billing dunning
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/billing-trial-expiration:
    post:
      tags:
        - Cron
      summary: Create billing trial expiration
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/compliance-archive:
    post:
      tags:
        - Cron
      summary: Deliver compliance journal digests
      description: Hourly. Delivers each archiving workspace's compliance journal as a daily digest (due-time based), re-syncs members' AngelSend archive once a day, and deletes journal entries past their retention.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/cron/contact-runway:
    get:
      tags:
        - Cron
      summary: Warn admins before a network cadence runs out of contacts (Vercel cron invokes crons with GET)
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Cron
      summary: Warn admins before a network cadence runs out of contacts
      description: Daily. Examines only workspaces with an enabled network cadence, and warns their admins (in-app and by email) when a cadence has fewer days of new contacts left than the workspace's threshold. At most one warning per workspace per 3 days; the claim is given back when the warning reached nobody.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/cron/daily-digest:
    post:
      tags:
        - Cron
      summary: Create daily digest
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/chat-events-prune:
    get:
      tags:
        - Cron
      summary: Run chat events prune (Vercel cron invokes crons with GET)
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Cron
      summary: Create chat events prune
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/error-log-prune:
    post:
      tags:
        - Cron
      summary: Create error log prune
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/integrations-refresh:
    post:
      tags:
        - Cron
      summary: Create integrations refresh
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/integrations-sync:
    post:
      tags:
        - Cron
      summary: Create integrations sync
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/orphan-projects-sweep:
    post:
      tags:
        - Cron
      summary: Create orphan projects sweep
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/ai-agent-headless-bills:
    post:
      tags:
        - Cron
      summary: Charge the durable bills of metered runs that died before settling
      description: |
        Every 15 minutes. Collects rows of app_ai_agent_headless_bills that are unsettled, have credits to charge, and have not been written to for 15 minutes — a run the platform killed before its `finally`. Idempotent per row (the charge replays on the row's own ledger reference). Bearer CRON_SECRET.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/recordings-reconcile:
    post:
      tags:
        - Cron
      summary: Create recordings reconcile
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/recordings-transcribe:
    post:
      tags:
        - Cron
      summary: Create recordings transcribe
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/recurring-tasks:
    post:
      tags:
        - Cron
      summary: Create recurring task
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/report-schedules:
    post:
      tags:
        - Cron
      summary: Create report schedule
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/scheduled-messages:
    post:
      tags:
        - Cron
      summary: Create scheduled message
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/sessions-cleanup:
    post:
      tags:
        - Cron
      summary: Create sessions cleanup
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/sla-breach-detector:
    post:
      tags:
        - Cron
      summary: Create sla breach detector
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/slip-risk-digest:
    post:
      tags:
        - Cron
      summary: Create slip risk digest
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/cron/stale-timers:
    post:
      tags:
        - Cron
      summary: Create stale timer
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/custom-fields:
    get:
      tags:
        - Custom Fields
      summary: List custom fields
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Custom Fields
      summary: Create custom field
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/custom-fields/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Custom Fields
      summary: Delete custom field
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    put:
      tags:
        - Custom Fields
      summary: Update custom field
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/task-emojis:
    get:
      tags:
        - Custom Statuses
      summary: List the workspace's custom task-emoji palette
      description: The emojis added on top of Kavanah's built-in on-card picker groups. This is a palette, not a constraint — app_tasks.emoji is free-form, so a task can carry any emoji whether or not it appears here.
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Custom Statuses
      summary: Add an emoji to the workspace's custom task-emoji palette
      description: Adding an emoji that is already present succeeds and returns alreadyExisted=true, so this is safe to call without checking first.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - emoji
              properties:
                emoji:
                  type: string
                  description: Exactly one emoji character (ZWJ and skin-tone sequences allowed).
                label:
                  type: string
                  description: What the emoji means in this workspace.
      responses:
        "200":
          description: Success
        "400":
          description: Not a single emoji
          or the workspace palette is full: null
        "401":
          description: Unauthorized
    delete:
      tags:
        - Custom Statuses
      summary: Remove an emoji from the workspace's custom task-emoji palette
      description: Only stops offering the emoji as a quick pick. Tasks already labelled with it keep it.
      parameters:
        - name: emoji
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "400":
          description: Missing emoji parameter
        "401":
          description: Unauthorized
  /api/custom-statuses:
    get:
      tags:
        - Custom Statuses
      summary: List custom statuses
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Custom Statuses
      summary: Create a custom status
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/custom-statuses/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Custom Statuses
      summary: Delete a custom status
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    put:
      tags:
        - Custom Statuses
      summary: Update a custom status
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/status-required-fields:
    delete:
      tags:
        - Custom Statuses
      summary: Delete status required fields
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Custom Statuses
      summary: List status required fields
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Custom Statuses
      summary: Create status required field
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/dashboard/refresh:
    get:
      tags:
        - Dashboard
      summary: List refresh
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/data-governance/consent:
    get:
      tags:
        - Data Governance
      summary: List consent
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Data Governance
      summary: Create consent
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/data-governance/cookie-consent:
    get:
      tags:
        - Data Governance
      summary: List cookie consent
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Data Governance
      summary: Create cookie consent
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/data-governance/data-requests:
    get:
      tags:
        - Data Governance
      summary: List data requests
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Data Governance
      summary: Create data request
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/data-governance/data-requests/{requestId}:
    parameters:
      - name: requestId
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Data Governance
      summary: Update data request
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/data-governance/dpa:
    get:
      tags:
        - Data Governance
      summary: List dpa
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Data Governance
      summary: Create dpa
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/data-governance/gdpr/delete:
    delete:
      tags:
        - Data Governance
      summary: Delete delete
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/data-governance/gdpr/export:
    get:
      tags:
        - Data Governance
      summary: List export
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/data-governance/workspace-export:
    get:
      tags:
        - Data Governance
      summary: Export the entire workspace as a JSON archive
      description: Admin/owner only. Reads across every workspace table, so it is rate limited to 5 requests per hour.
      parameters:
        - name: files
          in: query
          required: false
          description: How much file content to include.
          schema:
            type: string
            enum:
              - none
              - metadata
              - full
            default: full
      responses:
        "200":
          description: JSON archive
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          description: Unauthorized
        "403":
          description: Admin or owner required
        "404":
          description: No workspace found
        "429":
          description: Rate limited
  /api/data-governance/legal:
    get:
      tags:
        - Data Governance
      summary: List legal
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Data Governance
      summary: Create legal
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/data-governance/legal/accept:
    post:
      tags:
        - Data Governance
      summary: Create accept
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/data-governance/pii:
    get:
      tags:
        - Data Governance
      summary: List pii
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Data Governance
      summary: Create pii
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/data-governance/privacy-notice:
    get:
      tags:
        - Data Governance
      summary: List privacy notice
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Data Governance
      summary: Create privacy notice
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/trust/requests:
    post:
      tags:
        - Data Governance
      summary: Create request
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/dev:
    get:
      tags:
        - Dev Integration
      summary: List dev
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/dev/repos:
    get:
      tags:
        - Dev Integration
      summary: List repos
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Dev Integration
      summary: Create repo
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/angelsend/gmail/labels:
    get:
      tags:
        - Integrations
      summary: List labels
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/angelsend/gmail/messages:
    get:
      tags:
        - Integrations
      summary: List messages
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/angelsend/gmail/profile:
    get:
      tags:
        - Integrations
      summary: List profile
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/angelsend/gmail/send:
    post:
      tags:
        - Integrations
      summary: Create send
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/angelsend/gmail/status:
    get:
      tags:
        - Integrations
      summary: List status
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/angelsend/gmail/thread:
    get:
      tags:
        - Integrations
      summary: List thread
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/angelsend/oauth/callback:
    get:
      tags:
        - Integrations
      summary: List callback
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/angelsend/oauth/start:
    get:
      tags:
        - Integrations
      summary: List start
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/asana/tasks/{gid}:
    parameters:
      - name: gid
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Integrations
      summary: Delete task
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Integrations
      summary: Fetch task
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Integrations
      summary: Update task
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/asana/webhooks:
    post:
      tags:
        - Integrations
      summary: Create webhook
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/asana/webhooks/register:
    delete:
      tags:
        - Integrations
      summary: Delete register
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Integrations
      summary: List register
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Integrations
      summary: Create register
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/cloudcode/ai/chat:
    post:
      tags:
        - Integrations
      summary: Create chat
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/cloudcode/github/repos:
    get:
      tags:
        - Integrations
      summary: List repos
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/cloudcode/projects:
    get:
      tags:
        - Integrations
      summary: List projects
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Integrations
      summary: Create project
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/cloudcode/projects/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Integrations
      summary: Fetch project
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/cloudcode/projects/{id}/deployments:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Integrations
      summary: List deployments
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Integrations
      summary: Create deployment
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/cloudcode/projects/{id}/files:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Integrations
      summary: List files
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - Integrations
      summary: Update files
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/cloudcode/projects/{id}/files/{path}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: path
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Integrations
      summary: Delete file
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Integrations
      summary: Fetch file
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/gmail/messages/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Integrations
      summary: Delete message
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Integrations
      summary: Fetch message
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Integrations
      summary: Update message
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/gmail/push:
    post:
      tags:
        - Integrations
      summary: Create push
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/gmail/watch:
    delete:
      tags:
        - Integrations
      summary: Delete watch
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - Integrations
      summary: Create watch
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/google-calendar/events/{eventId}:
    parameters:
      - name: eventId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Integrations
      summary: Delete event
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Integrations
      summary: Fetch event
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Integrations
      summary: Update event
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/google-calendar/watch:
    post:
      tags:
        - Integrations
      summary: Create watch
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/google-calendar/watch/register:
    delete:
      tags:
        - Integrations
      summary: Delete register
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - Integrations
      summary: Create register
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/google-drive/changes:
    post:
      tags:
        - Integrations
      summary: Create change
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/google-drive/files/{fileId}:
    parameters:
      - name: fileId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Integrations
      summary: Delete file
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Integrations
      summary: Fetch file
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Integrations
      summary: Update file
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/google-drive/watch:
    delete:
      tags:
        - Integrations
      summary: Delete watch
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - Integrations
      summary: Create watch
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/multiverseai/agents:
    get:
      tags:
        - Integrations
      summary: List agents
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/multiverseai/agents/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Integrations
      summary: Fetch agent
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/multiverseai/agents/{id}/chat:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Integrations
      summary: Create chat
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/multiverseai/chat:
    post:
      tags:
        - Integrations
      summary: Create chat
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/multiverseai/models:
    get:
      tags:
        - Integrations
      summary: List models
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/multiverseai/workflows:
    get:
      tags:
        - Integrations
      summary: List workflows
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/multiverseai/workflows/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Integrations
      summary: Fetch workflow
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/integrations/multiverseai/workflows/{id}/execute:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Integrations
      summary: Create execute
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/scrappy/clients/{id}/push:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Integrations
      summary: Create push
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/scrappy/webhooks:
    post:
      tags:
        - Integrations
      summary: Create webhook
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/slack/events:
    post:
      tags:
        - Integrations
      summary: Create event
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/integrations/telegram/auth-test:
    get:
      tags:
        - Integrations
      summary: List auth test
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/firms:
    get:
      tags:
        - Deals
      summary: List institutions (the investor / issuer matrix)
      description: |
        Firms with AUM, dry powder, check size, thesis, sectors and a count of
        the contacts held at each. A firm is the entity that writes the cheque;
        a contact is a person at it.

        Filter by `role` (investor / issuer / broker_dealer / service_provider /
        other), `sector`, `min_aum_dollars`, or a `search` across name and
        thesis.
      parameters:
        - in: query
          name: role
          schema:
            type: string
        - in: query
          name: search
          schema:
            type: string
        - in: query
          name: sector
          schema:
            type: string
        - in: query
          name: min_aum_dollars
          schema:
            type: number
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
      responses:
        "200":
          description: "`{firms:[...]}`"
        "401":
          description: Unauthorized
    post:
      tags:
        - Deals
      summary: Create or update a firm
      description: |
        Resolves to an existing firm rather than creating a duplicate. DOMAIN is
        matched before name, because two funds can share a name but a mail
        domain belongs to one of them — so pass `domain` whenever it is known.
        Free-mail domains are ignored; they identify no firm.

        Money fields are supplied in DOLLARS and stored as integer cents.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                domain:
                  type: string
                  description: Accepts a bare host, a URL or an email address.
                roles:
                  type: array
                  items:
                    type: string
                    enum:
                      - investor
                      - issuer
                      - broker_dealer
                      - service_provider
                      - other
                website:
                  type: string
                linkedin_url:
                  type: string
                aum_dollars:
                  type: number
                dry_powder_dollars:
                  type: number
                check_size_min_dollars:
                  type: number
                check_size_max_dollars:
                  type: number
                thesis:
                  type: string
                sectors:
                  type: array
                  items:
                    type: string
                stages:
                  type: array
                  items:
                    type: string
                geographies:
                  type: array
                  items:
                    type: string
                notes:
                  type: string
      responses:
        "200":
          description: Matched an existing firm and updated it
        "201":
          description: Created
        "400":
          description: Missing name
          or an invalid role: null
        "401":
          description: Unauthorized
  /api/firms/{id}:
    get:
      tags:
        - Deals
      summary: One firm, plus every relationship held there
      description: Returns the firm and its contacts, each with a warmth score.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: "`{firm, contacts:[...]}`"
        "401":
          description: Unauthorized
        "404":
          description: Not found
    patch:
      tags:
        - Deals
      summary: Edit a firm
      description: |
        A human edit. Passing `lock_fields` marks columns as curated, after
        which enrichment will not overwrite them — the mechanism that stops an
        automated refresh quietly reverting a correction made by hand.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                domain:
                  type: string
                website:
                  type: string
                linkedin_url:
                  type: string
                aum_dollars:
                  type: number
                dry_powder_dollars:
                  type: number
                thesis:
                  type: string
                notes:
                  type: string
                lock_fields:
                  type: array
                  items:
                    type: string
                  description: Column names enrichment must not overwrite.
      responses:
        "200":
          description: Updated
        "400":
          description: Invalid body
        "401":
          description: Unauthorized
        "404":
          description: Not found
    delete:
      tags:
        - Deals
      summary: Archive a firm
      description: |
        Archives rather than deletes. Contacts survive with their firm link
        nulled — a person we know is not lost when their employer record is.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: "`{archived: boolean}`"
        "401":
          description: Unauthorized
  /api/firms/reconcile:
    get:
      tags:
        - Deals
      summary: How much of the workspace is in the institution layer
      description: |
        Total contacts, how many are attached to a firm, how many still carry a
        usable employer name with no firm behind it, and how many firms exist.
        Junk employer strings ("N/A", "self-employed") are excluded from the
        unlinked count, because reconciliation deliberately skips them and
        counting them would promise a fix that will not come.
      responses:
        "200":
          description: "`{contacts, linked, unlinkedWithCompany, firms}`"
        "401":
          description: Unauthorized
    post:
      tags:
        - Deals
      summary: Derive firms from contacts' free-text employer
      description: |
        Groups existing contacts by employer name, creates a firm for each, and
        attaches them. This is what turns "two contacts at the same fund are two
        strings that happen to match" into one firm with two relationships.

        Grouping is by NAME, with the domain inferred from the group's modal
        corporate address — so a contact on a personal address, or with no
        address at all, still lands at their firm. Domain-first grouping would
        silently drop exactly those people.

        Idempotent: only ever fills an EMPTY firm link, so a contact someone
        deliberately pointed elsewhere is never overruled. Pass `dry_run` to get
        the plan without writing.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                dry_run:
                  type: boolean
                limit:
                  type: integer
                  default: 5000
                roles:
                  type: array
                  items:
                    type: string
                  description: Roles applied to firms this run creates.
      responses:
        "200":
          description: "`{groupsExamined, firmsCreated, firmsMatched, contactsLinked, skipped, preview}`"
        "401":
          description: Unauthorized
  /api/firms/merge:
    post:
      tags:
        - Deals
      summary: Merge a duplicate firm into the one to keep
      description: |
        Admin only — it collapses two records for everyone in the workspace.

        FILL, NEVER OVERWRITE: empty fields on the kept firm take the
        duplicate's values, roles and list fields union, and a disagreement is
        REPORTED (`result.differences`) rather than resolved. Contacts,
        investor / broker-dealer matches, partner pairings and sheet import
        snapshots move to the kept firm; where both held a match on the same
        deal, the kept firm's row stays and a HUMAN decision on the duplicate's
        (dismissed, approached) carries over.

        The duplicate is archived with `merged_into_id` pointing at the winner,
        and every importer follows that pointer — without it the next hourly
        sheet sync re-creates what was just merged away.

        `result.mergeId` is what `POST /api/network/merges/{id}/undo` reverses.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - winner_id
                - loser_id
              properties:
                winner_id:
                  type: string
                  description: The firm to KEEP.
                loser_id:
                  type: string
                  description: The duplicate to archive.
      responses:
        "200":
          description: "`{ok, result}`"
        "400":
          description: Missing ids
        "401":
          description: Unauthorized
        "403":
          description: Not an admin
        "409":
          description: Refused — same record, another workspace's id, already merged, or archived
  /api/network/contacts/merge:
    post:
      tags:
        - Deals
      summary: Merge a duplicate contact into the one to keep
      description: |
        Admin only. The kept contact's empty fields are filled from the
        duplicate (tags union; last-touch timestamps take the later of the
        two); deal links, cadence enrollments, activity, document views and
        sheet snapshots re-point to it; the duplicate is archived and later
        imports follow its address to the kept contact.

        Where BOTH were enrolled in the same sequence the duplicate's
        enrollment is CANCELLED, so the person is not mailed twice — and an
        undo deliberately leaves it cancelled rather than resuming outbound
        email from an undo button.

        `result.mergeId` is what `POST /api/network/merges/{id}/undo` reverses.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - winner_id
                - loser_id
              properties:
                winner_id:
                  type: string
                loser_id:
                  type: string
      responses:
        "200":
          description: "`{ok, result}`"
        "400":
          description: Missing ids
        "401":
          description: Unauthorized
        "403":
          description: Not an admin
        "409":
          description: Refused — see the message
  /api/network/contacts/{id}/research:
    get:
      tags:
        - Deals
      summary: Verified research facts for a contact
      description: |
        Facts about the contact and its firm that passed the fact-check gate
        (verbatim quote on the source page, re-confirmed on an independent
        re-fetch, identity corroborated, judged to support the statement),
        each with source URL, quote, source date, retrieval/verification
        dates and expiry. Plus the last research runs with discard reasons.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
        - in: query
          name: include_expired
          schema:
            type: string
            enum:
              - "1"
          description: Also return expired and retracted facts.
      responses:
        "200":
          description: "`{facts, runs, autoResearchEnabled, estimatedCredits}`"
        "401":
          description: Unauthorized
        "404":
          description: No such contact
    post:
      tags:
        - Deals
      summary: Research a contact now
      description: |
        Runs the research pipeline for one contact and stores only facts that
        pass the fact-check. Spends credits (typically 25-40), billed to the
        caller; 402 when they cannot cover one run. LinkedIn is never scraped.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: "`{ok, outcome:{runId, pagesRead, candidates, kept, discarded, discardReasons, factIds}}`"
        "401":
          description: Unauthorized
        "402":
          description: Insufficient credits
        "404":
          description: No such contact
        "409":
          description: Researched in the last 24 hours
          or already running: null
  /api/network/contacts/{id}/research/facts/{factId}/retract:
    post:
      tags:
        - Deals
      summary: Retract a verified research fact
      description: |
        Admin only. The fact is never offered to an opener again; a later run
        that re-finds it leaves it retracted.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
        - in: path
          name: factId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: "`{ok}`"
        "401":
          description: Unauthorized
        "403":
          description: Not an admin
        "404":
          description: No active fact with that id for this contact
  /api/network/merges:
    get:
      tags:
        - Deals
      summary: Recent contact and firm merges
      description: Newest first, with whether each has been undone.
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        "200":
          description: "`{merges:[{id, kind, winnerId, loserId, mergedAt, undoneAt}]}`"
        "401":
          description: Unauthorized
  /api/network/merges/{id}/undo:
    post:
      tags:
        - Deals
      summary: Reverse a merge
      description: |
        Admin only. Restores the archived record and moves back everything the
        merge moved, reading the journal written in the merge's own
        transaction — never a payload the caller supplies.

        Per field, only a value still exactly as the merge left it is put back;
        anything edited since is KEPT and named in `result.keptFields`. A
        cadence enrollment the merge cancelled is NOT re-activated.

        409 when the records have moved on too far to reverse safely: the merge
        was already undone, the kept record was itself merged away (undo that
        one first), or another active record now holds the archived one's
        email / name / domain.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: "`{ok, result}`"
        "401":
          description: Unauthorized
        "403":
          description: Not an admin
        "409":
          description: Cannot be reversed safely
  /api/network/sheet-link:
    get:
      tags:
        - Deals
      summary: The Google Sheets linked to the network (firms + contacts)
      description: Every active link with its column mapping, mirror state and last import summary.
      responses:
        "200":
          description: "`{links:[...]}`"
        "401":
          description: Unauthorized
    post:
      tags:
        - Deals
      summary: Link a sheet tab to the network, and import it
      description: |
        With only `spreadsheet` this is a DRY RUN that lists the file's tabs.
        With `tab` it maps the headers (or takes `columnMapping`), saves the
        link — which is what turns on the HOURLY import — and runs the first
        import.

        The import GAP-FILLS: an empty Kavanah field takes the sheet's value, a
        field still holding what the sheet last supplied follows the sheet, and
        a value someone edited in Kavanah is never overwritten (it is counted
        as `fieldsKeptHandEdited`). Money cells accept "$1.2B", "250M",
        "$5M - $25M"; a cell that cannot be read is skipped and reported, never
        guessed.

        Linking never writes to the spreadsheet — only the mirror does.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - spreadsheet
              properties:
                spreadsheet:
                  type: string
                  description: Sheets URL or spreadsheet id.
                tab:
                  type: string
                  description: Omit to list the tabs.
                columnMapping:
                  type: object
                  description: header → canonical field (firm_name, firm_aum, contact_email, …). Omit to auto-guess.
      responses:
        "200":
          description: "`{tabs}` for a dry run, or `{ok, link, mapping, ignored, sync}`"
        "400":
          description: Unreadable spreadsheet
          unknown tab: null
          or a mapping that can identify nobody: null
        "401":
          description: Unauthorized
        "412":
          description: Google Sheets is not connected for the caller
    patch:
      tags:
        - Deals
      summary: Arm, disarm or rename the network mirror
      description: |
        The mirror rewrites a SEPARATE tab with Kavanah's whole network every
        hour. It is a destructive overwrite of that tab, so it REFUSES any tab
        a Kavanah sync reads from — this link's source tab or any sibling
        link's in the same file, compared case- and whitespace-insensitively.
        The refusal is repeated at write time, because the cron never passes
        through this route.

        Arming (or renaming) is limited to the member who linked the sheet, or
        an admin: the mirror writes with the linking member's Google grant.
        Turning it OFF is open to any member.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                linkId:
                  type: string
                  description: Required when more than one sheet is linked.
                mirrorEnabled:
                  type: boolean
                mirrorTabName:
                  type: string
      responses:
        "200":
          description: "`{link}`"
        "400":
          description: Refused — the mirror tab is a tab a sync reads from
        "401":
          description: Unauthorized
        "403":
          description: Not the linking member or an admin
        "404":
          description: No such link
        "409":
          description: Several links — name one with linkId
    delete:
      tags:
        - Deals
      summary: Unlink a network sheet
      description: |
        Soft: the hourly import and mirror stop, nothing already imported is
        removed, and the spreadsheet is untouched. Re-linking the same sheet
        and tab revives the SAME row, so its per-field import snapshots still
        apply — without them every value the sheet ever wrote would read as a
        hand edit and stop following the sheet. A revived link comes back with
        the mirror OFF unless it is armed again explicitly.
      parameters:
        - in: query
          name: link_id
          schema:
            type: string
      responses:
        "200":
          description: "`{unlinked: boolean}`"
        "401":
          description: Unauthorized
        "404":
          description: No such link
  /api/network/sheet-link/sync:
    post:
      tags:
        - Deals
      summary: Sync the network sheet now
      description: |
        The gap-filling import, then the mirror when it is armed. Body
        `{linkId?}`; omit it to sync every linked sheet. A per-link lease stops
        this from importing a sheet the hourly cron is already importing.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                linkId:
                  type: string
      responses:
        "200":
          description: "`{ok, outcomes:[{import, mirror, …}]}`"
        "401":
          description: Unauthorized
        "404":
          description: No sheet linked
        "409":
          description: Several links — name one with linkId
  /api/network/summary:
    get:
      tags:
        - Deals
      summary: Network counts and filter options
      description: Firms by role and partner status, the sectors in use, and how many contacts sit at a firm.
      responses:
        "200":
          description: "`{firms, byRole, byPartnerStatus, sectors, contacts, contactsAtFirms, viewer}`"
        "401":
          description: Unauthorized
  /api/deals/audience/schedule:
    get:
      tags:
        - Deals
      summary: The scheduled AngelSend audience sync
      description: |
        Which directions are on, whose AngelSend key runs them, and each
        direction's last run and result. Every direction defaults OFF; a
        workspace with no row runs nothing.
      responses:
        "200":
          description: "`{settings}`"
        "401":
          description: Unauthorized
    put:
      tags:
        - Deals
      summary: Turn the scheduled AngelSend directions on or off
      description: |
        Admin only, and it runs with the SAVING admin's AngelSend key — the job
        re-checks them as an active admin on every run and pauses itself if
        they are not. Only the fields sent change.

        `optoutsInEnabled` (hourly) imports AngelSend's do-not-email list into
        this workspace's suppression list. `contactsPullEnabled` (daily)
        gap-fills Deal Room contacts from a segment (paged) or the newest 500
        leads. `contactsPushEnabled` (daily) is CREATE-ONLY: it sends only
        addresses AngelSend has never held, because AngelSend's import
        overwrites the whole lead row — status, tags and notes included.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                optoutsInEnabled:
                  type: boolean
                contactsPullEnabled:
                  type: boolean
                pullSegmentId:
                  type: string
                  nullable: true
                pullProjectId:
                  type: string
                  nullable: true
                contactsPushEnabled:
                  type: boolean
                pushProjectId:
                  type: string
                  nullable: true
                  description: Required to turn the push on.
      responses:
        "200":
          description: "`{settings}`"
        "400":
          description: Nothing to push into
          or the caller's AngelSend key is unusable: null
        "401":
          description: Unauthorized
        "403":
          description: Not an admin
  /api/integrations/docsend/ingest:
    get:
      tags:
        - Integrations
      summary: DocSend ingest status for the workspace
      description: |
        Whether a DocSend ingest token exists, its last four characters, and
        when a view event last arrived. Never returns the token itself — it is
        stored hashed and is readable exactly once, in the POST response.

        `lastEventAt` is the field that matters operationally: a token that
        exists but has never received an event means the relay was never
        finished, which is otherwise indistinguishable from "nobody has opened
        the document".
      responses:
        "200":
          description: "`{configured, enabled, tokenHint, lastEventAt}`"
        "401":
          description: Unauthorized
    post:
      tags:
        - Integrations
      summary: Mint or rotate the DocSend ingest token
      description: |
        Issues a new ingest token and returns the full webhook URL to point a
        DocSend relay (typically a Zap) at. This is the ONLY response that ever
        carries the raw token. Rotating invalidates the previous URL
        immediately, which is the recovery path for a token that leaked through
        a Zapier task history or a screenshot.

        Admin only — the URL is a workspace-wide write credential.
      responses:
        "200":
          description: "`{tokenHint, webhookUrl, note}`"
        "401":
          description: Unauthorized
        "403":
          description: Admin role required
    delete:
      tags:
        - Integrations
      summary: Disable the DocSend webhook path
      description: |
        Disables the webhook token. Deliberately a disable rather than a
        delete, so `lastEventAt` survives and "this used to work" stays
        answerable. Does not affect the notification-email ingest path, which
        has no token.
      responses:
        "200":
          description: Disabled
        "401":
          description: Unauthorized
        "403":
          description: Admin role required
  /api/integrations/docsend/webhook/{token}:
    post:
      tags:
        - Integrations
      summary: Inbound DocSend view event
      description: |
        Receives a DocSend document-view event. DocSend itself cannot call this
        — it publishes no API and no native webhooks — so the caller is
        normally a Zap (DocSend trigger, "Webhooks by Zapier" POST action) or a
        relay the customer runs.

        Authenticated by the token in the path, resolved by hash. Field names
        are alias-tolerant (`document_name` / `document` / `link_name` /
        `title`; `visitor_email` / `email` / `viewer_email`; and so on) because
        the body shape is whatever the customer's Zap was configured to send.

        A duplicate view answers 200, not an error: the sender retries on any
        non-2xx, and retrying an already-recorded visit would never converge.
        An unauthorized or unknown token answers 404, indistinguishable from a
        wrong URL.
      security: []
      parameters:
        - in: path
          name: token
          required: true
          schema:
            type: string
          description: The workspace's ingest token.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Alias-tolerant view event; a document name is the only hard requirement.
      responses:
        "200":
          description: '`{ok:true, status:"recorded"|"duplicate"|"ignored"}`'
        "400":
          description: Body was not a JSON object
          or carried no recognisable document name: null
        "404":
          description: Unknown or disabled ingest token
  /api/integrations/angelsend/webhook/{token}:
    post:
      tags:
        - Integrations
      summary: Inbound AngelSend webhook delivery
      description: |
        Receives an AngelSend outbound-webhook event (gateway 9edd7902). Kavanah
        registers the subscription itself — the AngelSend sync ensures one per
        connection, needing a key with write scope — for `meeting.booked`,
        `contact.unsubscribed`, `message.bounced`, `message.replied` and
        `recipient.stopped`. A delivery names no account, so the URL carries a
        per-connection routing token; the body is authenticated by
        `X-AngelSend-Signature: t=<unix seconds>,v1=<hex>` — HMAC-SHA256 of
        `<t>.<raw body>` keyed by the UTF-8 bytes of the secret AngelSend
        returned at registration — verified constant-time inside a ±300 s window.

        A booking is recorded through the same recorder as the poll (keyed on
        its id); an unsubscribe goes on the workspace suppression list and
        stops outreach; a hard bounce goes on the global list and stops
        outreach; a reply or stopped recipient wakes the campaign mirror. Every
        handler is idempotent, so AngelSend's retries and duplicates are
        harmless. Work runs after the response. `webhook.test` is acknowledged
        and ignored.
      security: []
      parameters:
        - in: path
          name: token
          required: true
          schema:
            type: string
          description: The connection's routing token (not a credential — the HMAC is).
        - in: header
          name: x-angelsend-signature
          required: true
          schema:
            type: string
        - in: header
          name: x-angelsend-event-id
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: An AngelSend delivery `{ id, event, occurred_at, project_id, data }`.
      responses:
        "200":
          description: "`{ok:true}` — also for events Kavanah does not act on, and `{ok:true,test:true}` for webhook.test"
        "400":
          description: Signature missing/stale/invalid
          or the body is not JSON (final — AngelSend does not retry 4xx): null
        "404":
          description: Unknown routing token
  /api/integrations/xdocs/webhook/{token}:
    post:
      tags:
        - Integrations
      summary: Inbound XDocs webhook delivery
      description: |
        Receives an XDocs event (envelope lifecycle, tracked-link reads). The
        subscription is created automatically when a user connects XDocs with
        a key that holds `documents:write`; the URL carries a per-connection
        routing token, and the body is authenticated by the `x-xdocs-signature`
        header — a hex HMAC-SHA256 of `{x-xdocs-timestamp}.{raw body}` under
        the secret XDocs returned at subscribe time — verified constant-time
        inside a ±300 s window.

        The payload is treated as a wake-up, never as truth: the route reads
        ids out of it and re-fetches the envelope or view from the XDocs API
        with the connection's own key before writing anything. Deal timeline
        activity is keyed so a replay is a no-op. An unknown token answers 404,
        indistinguishable from a wrong URL; a bad signature answers 400 and
        writes nothing; an event with nothing to act on answers 200.
      security: []
      parameters:
        - in: path
          name: token
          required: true
          schema:
            type: string
          description: The connection's routing token (not a credential — the HMAC is).
        - in: header
          name: x-xdocs-signature
          required: true
          schema:
            type: string
        - in: header
          name: x-xdocs-timestamp
          required: true
          schema:
            type: string
        - in: header
          name: x-xdocs-event-id
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: An XDocs webhook delivery; only `event` and the object ids are read.
      responses:
        "200":
          description: "`{ok:true}` — also for ignored events and `{ok:true, pong:true}` for a ping"
        "400":
          description: Signature missing/stale/invalid
          or the body is not JSON: null
        "404":
          description: Unknown routing token
        "500":
          description: The XDocs re-fetch or the Kavanah write failed; XDocs will retry
  /api/integrations/google-meet/imports:
    get:
      tags:
        - Integrations
      summary: The caller's Google Meet transcript import — settings and imported calls
      description: |
        Whether the workspace turned on the Google Meet transcript import
        (`google_meet_import_enabled`, Settings → AI Agent → Oversight),
        whether the caller opted out, whether their Google grant needs
        re-consent, and the caller's OWN imported calls, newest first (title,
        start, status, notes summary, created task ids, recording links, the
        linked deal). Never another member's calls.
      responses:
        "200":
          description: "`{ok:true, settings:{workspaceEnabled, optedOut, scopeStatus, lastError, lastDiscoveredAt}, imports:[…]}`"
        "401":
          description: Unauthorized
    patch:
      tags:
        - Integrations
      summary: Opt the caller out of (or back into) the Google Meet transcript import
      description: |
        Per-member opt-out within the workspace. Turning it on also stops the
        caller's calls still in flight (they become `stopped`); calls already
        imported are kept.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - optedOut
              properties:
                optedOut:
                  type: boolean
      responses:
        "200":
          description: "`{ok:true, settings}` — the caller's settings after the change"
        "400":
          description: "`optedOut` missing or not a boolean"
        "401":
          description: Unauthorized
  /api/integrations/google-meet/imports/{id}:
    get:
      tags:
        - Integrations
      summary: One of the caller's imported Google Meet calls, with its transcript
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: "`{ok:true, import:{…, transcriptText, participants}}`"
        "401":
          description: Unauthorized
        "404":
          description: No such import for the caller in this workspace (another member's import is also a 404)
  /api/integrations/telegram/link:
    get:
      tags:
        - Integrations
      summary: Get (or issue) the caller's Telegram deep link
      description: |
        Returns the signed-in user's Telegram binding. When the user is not yet
        linked, issues a single-use `t.me/<bot>?start=<token>` deep link that
        binds their Telegram chat to their Kavanah account once they press
        Start. The bot itself is installed once per workspace (a BotFather
        token on the Telegram integration); the link is per user.
      responses:
        "200":
          description: Either `{status:"connected", botUsername, link}` or `{status:"pending", botUsername, deepLink, token}`
        "401":
          description: Unauthorized
        "412":
          description: No Telegram bot installed in this workspace
          or the install has no username yet: null
    delete:
      tags:
        - Integrations
      summary: Unlink the caller's Telegram chat
      responses:
        "200":
          description: Revoked (`revoked:false` when nothing was linked)
        "401":
          description: Unauthorized
  /api/integrations/telegram/webhook:
    post:
      tags:
        - Integrations
      summary: Telegram Bot API webhook (called by Telegram, not by clients)
      description: |
        Receives Bot API Updates. Authenticated by the
        `X-Telegram-Bot-Api-Secret-Token` header matching the secret minted at
        setWebhook time — not by a session or API key. Acknowledges with 200
        immediately and runs the agent turn after the response; a redelivered
        `update_id` is a no-op. Private chats only.
      parameters:
        - in: header
          name: X-Telegram-Bot-Api-Secret-Token
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              description: A Telegram Update object
      responses:
        "200":
          description: Acknowledged (body may carry `skipped` with the reason the update was not acted on)
        "400":
          description: Invalid JSON
        "403":
          description: Missing or unknown secret token
  /api/integrations/elevenlabs/webhook:
    post:
      tags:
        - Integrations
      summary: ElevenLabs post-call webhook (called by ElevenLabs, not by clients)
      description: |
        Receives ElevenLabs Conversational-AI post-call events. Authenticated
        by the `ElevenLabs-Signature` header (`t=<unix seconds>,v0=<hex
        HMAC-SHA256 of "<t>.<raw body>">`, keyed with
        `ELEVENLABS_CONVAI_WEBHOOK_SECRET`, 30-minute window) — not by a
        session or API key; a missing secret answers 503. A
        `post_call_transcription` event is matched to the session Kavanah
        minted (by the `kavanah_session_id` dynamic variable the client
        attached, or the `conversation_id` a previous delivery stamped —
        never by a guessed payer), and the minutes used beyond the one
        billed at mint are charged to that payer under `<mint
        reference>:final`; a replayed delivery charges nothing, an unmatched
        call is acknowledged and bills nobody. Other event types are
        acknowledged and ignored. A fault of Kavanah's own answers 503 so
        ElevenLabs retries.
      parameters:
        - in: header
          name: ElevenLabs-Signature
          required: true
          schema:
            type: string
            example: t=1759900000,v0=5f1e…
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              properties:
                type:
                  type: string
                  example: post_call_transcription
                event_timestamp:
                  type: integer
                data:
                  type: object
                  additionalProperties: true
                  properties:
                    conversation_id:
                      type: string
                    agent_id:
                      type: string
                    user_id:
                      type: string
                      description: The `userId` the client passed at session start
                    metadata:
                      type: object
                      additionalProperties: true
                      properties:
                        call_duration_secs:
                          type: number
                        start_time_unix_secs:
                          type: integer
      responses:
        "200":
          description: Acknowledged (`matched`, `replay` or `ignored` say what was done; a matched session reports `minutes`, `delta_minutes`, `delta_credits`, `charged`)
        "400":
          description: Invalid JSON
        "401":
          description: Missing
          malformed: null
          stale or wrong signature: null
        "413":
          description: Payload too large
        "503":
          description: ELEVENLABS_CONVAI_WEBHOOK_SECRET is not set
  /api/integrations/kanvo/agent:
    post:
      tags:
        - Integrations
      summary: Kanvo connected-agent endpoint (called by Kanvo, not by clients)
      description: |
        Kanvo's "connected agents" feature POSTs each message in a Kanvo thread
        here and posts the answer into the chat as the agent. Authenticated by
        the Kanvo user's own Kavanah API key, configured in Kanvo as the
        credential header `Authorization: Bearer kvn_live_…`; app keys and
        browser sessions are refused. Each Kanvo thread (and branch) is one
        Kavanah agent conversation. A `ping` (Kanvo's "Send test") answers a
        greeting without running a turn. A delivery whose `action.id` is
        `undo:<group id>` (the Undo button on a "Done" card) reverses that
        agent turn instead of running one. Runs a full agent turn (credits are
        charged) stopped at ~80s, since Kanvo waits 100s. Setup:
        docs/kanvo-integration.md.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              description: A Kanvo delivery (`message.created` or `ping`)
      responses:
        "200":
          description: "`{ text, cards? }` — refusals (credits, rate limit, not the key's owner) are also a 200 with a sentence in `text`"
        "400":
          description: The body is not a JSON object
        "401":
          description: No Kavanah API key, or an invalid or revoked one
        "403":
          description: An app key (`kvn_app_…`), or the workspace requires two-factor setup
  /api/water-cooler/threads:
    get:
      tags:
        - AI Agent
      summary: List Water Cooler threads
      responses:
        "200":
          description: Threads
          newest activity first: null
        "401":
          description: Unauthorized
    post:
      tags:
        - AI Agent
      summary: Start a Water Cooler conversation
      description: |
        Opens a thread and runs the first exchange after the response: the
        selected AI employees (2-4) debate the topic for the chosen rounds and
        close with a "so what". Billed to the caller's credits like a chat
        turn. Poll the GET below while `status` is `running`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - topic
                - persona_ids
              properties:
                topic:
                  type: string
                  maxLength: 2000
                persona_ids:
                  type: array
                  items:
                    type: string
                  minItems: 2
                  maxItems: 4
                rounds:
                  type: integer
                  enum:
                    - 1
                    - 2
                  default: 1
      responses:
        "202":
          description: Thread created; the exchange runs after the response
        "400":
          description: Invalid topic / seats
        "403":
          description: AI agent disabled for the workspace
        "429":
          description: Hourly workspace conversation ceiling reached
  /api/water-cooler/threads/{id}:
    get:
      tags:
        - AI Agent
      summary: Get a thread with its messages
      responses:
        "200":
          description: Thread
          messages: null
          and the persona/human name maps: null
        "404":
          description: Not found
    delete:
      tags:
        - AI Agent
      summary: Delete a thread (starter or admin)
      responses:
        "200":
          description: Deleted
        "403":
          description: Not the starter and not an admin
        "404":
          description: Not found
  /api/water-cooler/threads/{id}/reply:
    post:
      tags:
        - AI Agent
      summary: Reply as a human (re-wakes the room when idle)
      description: |
        Records the reply immediately. If no exchange is running, re-wakes the
        seats for one reply round plus a fresh close, billed to the replier.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                content:
                  type: string
                  maxLength: 4000
      responses:
        "202":
          description: Saved; `rewoke` says whether an exchange started
        "404":
          description: Not found
  /api/water-cooler/threads/{id}/promote:
    post:
      tags:
        - AI Agent
      summary: Turn the thread's close into a task
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - project_ref
              properties:
                project_ref:
                  type: string
                title:
                  type: string
                  maxLength: 200
      responses:
        "200":
          description: Created; returns task_id
        "409":
          description: The thread has no close yet
  /api/invites/{token}:
    parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Invites
      summary: Fetch invite
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/invites/{token}/accept:
    parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Invites
      summary: Create accept
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "402":
          description: The workspace has no free seat (`plan_limit_reached`). The invite stays pending; the same link works once a seat frees.
  /api/invites/{token}/decline:
    parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Invites
      summary: Create decline
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/invites/{token}/request-resend:
    parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Invites
      summary: Create request resend
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "402":
          description: An expired invite would take a seat back and none is free (`plan_limit_reached`)
  /api/invites/mine:
    get:
      tags:
        - Invites
      summary: List mine
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/knowledge/ask:
    get:
      tags:
        - Knowledge
      summary: List ask
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Knowledge
      summary: Create ask
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/knowledge/autolink:
    get:
      tags:
        - Knowledge
      summary: List autolink
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Knowledge
      summary: Create autolink
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/knowledge/autolink/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Knowledge
      summary: Delete autolink
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - Knowledge
      summary: Update autolink
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/knowledge/autolink/tokenize:
    post:
      tags:
        - Knowledge
      summary: Create tokenize
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/knowledge/index-stats:
    get:
      tags:
        - Knowledge
      summary: List index stats
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Knowledge
      summary: Create index stat
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/knowledge/readme:
    get:
      tags:
        - Knowledge
      summary: List readme
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Knowledge
      summary: Create readme
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/knowledge/decision-candidates:
    get:
      tags:
        - Knowledge
      summary: List AI-suggested decision candidates
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum:
              - pending
              - accepted
              - dismissed
              - all
            default: pending
          description: Filter by review state.
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Knowledge
      summary: Scan documents for decision candidates (paginated; re-call with the returned offset until hasMore is false)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                offset:
                  type: integer
                  minimum: 0
                  description: Pagination offset from a prior call.
                limit:
                  type: integer
                  minimum: 1
                  maximum: 25
                  description: Documents to scan this call.
      responses:
        "200":
          description: "Scan result: scanned/found/hasMore/offset plus the pending candidates"
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "409":
          description: A scan is already running for this workspace
        "502":
          description: LLM error
        "503":
          description: No LLM configured
        "504":
          description: LLM timeout
  /api/knowledge/decision-candidates/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Knowledge
      summary: Accept or dismiss a decision candidate (terminal — accepting materializes a real decision record)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
              properties:
                action:
                  type: string
                  enum:
                    - accept
                    - dismiss
                decision:
                  type: object
                  description: Optional reviewer edits applied when accepting.
                  properties:
                    title:
                      type: string
                    context:
                      type: string
                    whatWeChose:
                      type: string
                    decidedAt:
                      type: string
                    status:
                      type: string
                    decidedBy:
                      type: string
      responses:
        "200":
          description: Success
        "400":
          description: Invalid action
        "401":
          description: Unauthorized
        "404":
          description: Candidate not found
        "409":
          description: Candidate was already reviewed
  /api/knowledge/stale:
    get:
      tags:
        - Knowledge
      summary: List stale
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Knowledge
      summary: Create stale
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/knowledge/stale/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Knowledge
      summary: Update stale
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/knowledge/threads/condensable:
    get:
      tags:
        - Knowledge
      summary: List condensable
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/knowledge/threads/condense:
    post:
      tags:
        - Knowledge
      summary: Create condense
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/legal-holds:
    get:
      tags:
        - Legal Holds
      summary: List legal holds
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Legal Holds
      summary: Create legal hold
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/legal-holds/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Legal Holds
      summary: Delete legal hold
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/stt:
    post:
      tags:
        - Media
      summary: Create stt
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tts:
    get:
      tags:
        - Media
      summary: List tts
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/tts/speak:
    post:
      tags:
        - Media
      summary: Create speak
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/audit:
    get:
      tags:
        - Messages
      summary: List audit
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/audit/exports:
    get:
      tags:
        - Messages
      summary: List exports
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Create export
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/audit/retention:
    get:
      tags:
        - Messages
      summary: List retention
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Messages
      summary: Update retention
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/channels:
    get:
      tags:
        - Messages
      summary: List channels
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Create channel
      description: Creates a channel with the caller as its admin. Optional `memberIds` seeds initial members — ids are filtered to active members of the caller's workspace (ineligible ids are dropped, not an error).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                description:
                  type: string
                isPrivate:
                  type: boolean
                projectRef:
                  type: string
                  nullable: true
                memberIds:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/channels/{channelId}:
    parameters:
      - name: channelId
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Messages
      summary: Update channel
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/channels/{channelId}/members:
    parameters:
      - name: channelId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Messages
      summary: Leave a channel or remove a member
      description: With no body (or your own userId), leaves the channel — always allowed. With another user's id (JSON body `userId` or the `userId` query parameter), removes that member — workspace admins, channel admins, or the channel creator only.
      parameters:
        - name: userId
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                userId:
                  type: string
      responses:
        "200":
          description: Membership removed (idempotent)
        "403":
          description: Caller may not remove that member
        "404":
          description: Not found
    get:
      tags:
        - Messages
      summary: List members
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Join a channel or add a member
      description: With no body (or your own userId), joins the channel — public channels only; private channels require an invite. With another user's id (JSON body `userId`), adds that workspace member — the caller must be a channel member or a workspace admin, and this is the only way into a private channel. Adding an existing member is an idempotent no-op.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                userId:
                  type: string
      responses:
        "201":
          description: Member added / channel joined
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "403":
          description: Caller may not add members / private channel requires invite
        "404":
          description: Not found
  /api/messages/client-channels:
    get:
      tags:
        - Messages
      summary: List client channels
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Create client channel
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/client-channels/{channelId}/members:
    parameters:
      - name: channelId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Messages
      summary: Delete members
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Messages
      summary: List members
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Create member
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/client-channels/{channelId}/messages:
    parameters:
      - name: channelId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Messages
      summary: List messages
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Create message
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/files:
    get:
      tags:
        - Messages
      summary: List files
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/files/{messageId}/{attachmentId}:
    parameters:
      - name: messageId
        in: path
        required: true
        schema:
          type: string
      - name: attachmentId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Messages
      summary: Download a message attachment
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/mentions:
    get:
      tags:
        - Messages
      summary: List mentions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/notifications:
    get:
      tags:
        - Messages
      summary: List notifications
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Messages
      summary: Update notifications
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/presence:
    post:
      tags:
        - Messages
      summary: Create presence
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/project-links:
    delete:
      tags:
        - Messages
      summary: Delete project links
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Messages
      summary: List project links
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Create project link
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/push/subscribe:
    delete:
      tags:
        - Messages
      summary: Delete subscribe
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - Messages
      summary: Create subscribe
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/push/vapid-key:
    get:
      tags:
        - Messages
      summary: List vapid key
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/scheduled:
    get:
      tags:
        - Messages
      summary: List scheduled
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Create scheduled
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/scheduled-processor:
    post:
      tags:
        - Messages
      summary: Create scheduled processor
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/scheduled/{messageId}:
    parameters:
      - name: messageId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Messages
      summary: Delete scheduled
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - Messages
      summary: Update scheduled
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/search:
    get:
      tags:
        - Messages
      summary: List search
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/stream:
    get:
      tags:
        - Messages
      summary: List stream
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/templates:
    get:
      tags:
        - Messages
      summary: List templates
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Create template
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/templates/{templateId}:
    parameters:
      - name: templateId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Messages
      summary: Delete template
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Messages
      summary: Fetch template
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Messages
      summary: Update template
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Action on template
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/rtc-config:
    get:
      tags:
        - Messages
      summary: ICE servers for the chat call transport
      description: STUN/TURN servers the browser should build its RTCPeerConnection with. Authenticated because TURN credentials are relay credentials. An empty list means "use public STUN only".
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/threads/{contactId}/meeting/recording:
    parameters:
      - name: contactId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Messages
      summary: List recording
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Create recording
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/threads/{contactId}/meeting/recording/{recordingId}:
    parameters:
      - name: contactId
        in: path
        required: true
        schema:
          type: string
      - name: recordingId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Messages
      summary: Delete recording
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Messages
      summary: Fetch recording
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/messages/threads/{contactId}/meeting/recording/{recordingId}/chunk:
    parameters:
      - name: contactId
        in: path
        required: true
        schema:
          type: string
      - name: recordingId
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Messages
      summary: Create chunk
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/threads/{contactId}/meeting/recording/{recordingId}/consent:
    parameters:
      - name: contactId
        in: path
        required: true
        schema:
          type: string
      - name: recordingId
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Messages
      summary: Create consent
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/threads/{contactId}/meeting/recording/{recordingId}/stop:
    parameters:
      - name: contactId
        in: path
        required: true
        schema:
          type: string
      - name: recordingId
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Messages
      summary: Create stop
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/threads/{contactId}/meeting/recording/{recordingId}/transcript:
    parameters:
      - name: contactId
        in: path
        required: true
        schema:
          type: string
      - name: recordingId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Messages
      summary: List transcript
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Create transcript
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/threads/{contactId}/messages/{messageId}/read:
    parameters:
      - name: contactId
        in: path
        required: true
        schema:
          type: string
      - name: messageId
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Messages
      summary: Create read
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/threads/{contactId}/messages/{messageId}/replies:
    parameters:
      - name: contactId
        in: path
        required: true
        schema:
          type: string
      - name: messageId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Messages
      summary: List replies
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Messages
      summary: Reply in a message thread
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/threads/{contactId}/messages/forward:
    parameters:
      - name: contactId
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Messages
      summary: Create forward
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/messages/threads/{contactId}/pinned:
    parameters:
      - name: contactId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Messages
      summary: List pinned
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/outreach/unsubscribe:
    get:
      tags:
        - Outreach
      summary: The unsubscribe confirmation page for cadence mail
      description: |
        PUBLIC and CSRF-exempt: the signed `t` token names one (workspace, address) and is the only credential. A GET NEVER unsubscribes — link scanners and mail gateways prefetch every URL in an inbound message — it renders a confirmation page whose button POSTs here (RFC 8058).
      security: []
      parameters:
        - name: t
          in: query
          required: true
          schema:
            type: string
          description: Signed unsubscribe token from the email.
      responses:
        "200":
          description: HTML confirmation page
        "400":
          description: HTML page — the token is not one we signed
        "429":
          description: Too many requests
    post:
      tags:
        - Outreach
      summary: Unsubscribe an address from a workspace's outreach (RFC 8058 one-click)
      description: |
        Called by the recipient's mail provider (`List-Unsubscribe-Post: List-Unsubscribe=One-Click`) or by the confirmation page's button. Adds the address to THIS workspace's suppression list (never the global one) and stops every active cadence enrollment for it, pulling back the steps already queued. Idempotent.
      security: []
      parameters:
        - name: t
          in: query
          required: true
          schema:
            type: string
          description: Signed unsubscribe token from the email.
      responses:
        "200":
          description: HTML page confirming the address will not be emailed again
        "400":
          description: HTML page — the token is not one we signed
        "429":
          description: Too many requests
        "500":
          description: HTML page — the request could not be recorded; the client should retry
  /api/notifications/admin/escalation-rules:
    delete:
      tags:
        - Notifications
      summary: Delete escalation rules
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Notifications
      summary: List escalation rules
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Notifications
      summary: Update escalation rules
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Notifications
      summary: Create escalation rule
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/notifications/admin/health-alerts:
    delete:
      tags:
        - Notifications
      summary: Delete health alerts
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Notifications
      summary: List health alerts
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Notifications
      summary: Update health alerts
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Notifications
      summary: Create health alert
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/notifications/admin/metrics:
    get:
      tags:
        - Notifications
      summary: List metrics
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/notifications/admin/rules:
    delete:
      tags:
        - Notifications
      summary: Delete rules
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Notifications
      summary: List rules
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Notifications
      summary: Update rules
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Notifications
      summary: Create rule
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/notifications/admin/teams:
    delete:
      tags:
        - Notifications
      summary: Delete teams
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Notifications
      summary: List teams
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Notifications
      summary: Create team
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/notifications/sms/verify:
    get:
      tags:
        - Notifications
      summary: Get the caller's SMS enrolment state
      description: Whether the caller has a phone enrolled for SMS alerts, and how far through verification it is. The number is MASKED — the settings panel only needs to know which handset is enrolled, and echoing a full phone number back through an API is a needless way to leak one into a log or a browser cache. `pending` reports whether an unexpired code is outstanding, so the UI can reopen on the code box after a reload. Always scoped to the caller.
      responses:
        "200":
          description: Enrolment state (phone is null when nothing is enrolled)
        "401":
          description: Unauthorized
    post:
      tags:
        - Notifications
      summary: Start or confirm SMS verification
      description: "Send `{ phone }` to start verification (delivers a code by SMS), or `{ code }` to confirm one already sent. Deliberately has no userId parameter and is always scoped to the CALLER: enrolling a phone is proof that the person holds that handset, so letting one user start verification for another would make the flow meaningless. Sending a code costs money and is subject to the workspace SMS opt-in and the monthly segment cap, even though it bypasses the notification preference and quiet-hours gates (it is transactional — the user is on the settings screen asking for it)."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required:
                    - phone
                  properties:
                    phone:
                      type: string
                      description: E.164 number to enrol, e.g. "+15555550100"
                - type: object
                  required:
                    - code
                  properties:
                    code:
                      type: string
                      description: The verification code received by SMS
      responses:
        "200":
          description: Code sent
          or verification confirmed: null
        "400":
          description: Invalid phone number or code
        "401":
          description: Unauthorized
        "429":
          description: Too many verification attempts
        "503":
          description: SMS is not configured
          or the workspace has not opted in: null
    delete:
      tags:
        - Notifications
      summary: Forget the caller's enrolled number
      description: Remove the caller's phone entirely, ending SMS alerts for them. Scoped to the caller. Note this is distinct from a carrier STOP, which records an opt-out against the number rather than deleting the enrolment.
      responses:
        "200":
          description: Number removed
        "401":
          description: Unauthorized
  /api/notifications/admin/workspace-defaults:
    get:
      tags:
        - Notifications
      summary: List workspace defaults
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Notifications
      summary: Update workspace defaults
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/notifications/digest:
    get:
      tags:
        - Notifications
      summary: List digest
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Notifications
      summary: Update digest
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/notifications/other-workspaces:
    get:
      tags:
        - Notifications
      summary: Unread notification counts for the user's OTHER workspaces
      description: |
        The /notifications page renders one workspace, but the sidebar pill and OS dock badge count unread across every workspace the user belongs to. Returns the breakdown for the workspaces the page is not showing, so the badge is explainable without either scoping it down or hiding the notifications. The selected workspace is excluded — its unread is on the page itself.
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/notifications/dispatch:
    post:
      tags:
        - Notifications
      summary: Create dispatch
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/notifications/email-event:
    post:
      tags:
        - Notifications
      summary: Create email event
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/notifications/muted-threads:
    delete:
      tags:
        - Notifications
      summary: Delete muted threads
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Notifications
      summary: List muted threads
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Notifications
      summary: Create muted thread
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/notifications/preferences:
    get:
      tags:
        - Notifications
      summary: List preferences
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Notifications
      summary: Update preferences
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/notifications/process:
    post:
      tags:
        - Notifications
      summary: Process pending notifications
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/notifications/push-tokens:
    delete:
      tags:
        - Notifications
      summary: Delete push tokens
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Notifications
      summary: List push tokens
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Notifications
      summary: Create push token
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/notifications/quiet-hours:
    get:
      tags:
        - Notifications
      summary: List quiet hours
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Notifications
      summary: Update quiet hours
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/notifications/subscriptions:
    delete:
      tags:
        - Notifications
      summary: Delete subscriptions
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Notifications
      summary: List subscriptions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Notifications
      summary: Create subscription
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/notifications/unsubscribe:
    get:
      tags:
        - Notifications
      summary: List unsubscribe
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Notifications
      summary: Create unsubscribe
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/onboarding/config:
    get:
      tags:
        - Onboarding
      summary: List config
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - Onboarding
      summary: Update config
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/onboarding/progress:
    get:
      tags:
        - Onboarding
      summary: List progress
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Onboarding
      summary: Update progress
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/onboarding/sample-data:
    get:
      tags:
        - Onboarding
      summary: Get sample-data install status
      description: Whether the illustrative sample dataset is installed in the current workspace, and how many rows an install writes. Admin only.
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
        "403":
          description: Admin access required
    post:
      tags:
        - Onboarding
      summary: Install sample data
      description: Seed the current workspace with a small, clearly-labelled illustrative dataset (one project with a populated board, two clients carrying ARR and renewal dates, one knowledge page). Admin only. Idempotent by workspace — a second call returns 409 rather than seeding twice.
      responses:
        "201":
          description: Installed
        "401":
          description: Unauthorized
        "402":
          description: Workspace is at its project limit
        "403":
          description: Admin access required
        "409":
          description: Sample data is already installed
    delete:
      tags:
        - Onboarding
      summary: Remove sample data
      description: Delete exactly the rows the install recorded, and nothing else. Admin only.
      responses:
        "200":
          description: Removed
        "401":
          description: Unauthorized
        "403":
          description: Admin access required
        "404":
          description: No sample data installed
  /api/planning/budget-burn:
    get:
      tags:
        - Planning
      summary: List budget burn
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/planning/calibration:
    get:
      tags:
        - Planning
      summary: List calibration
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/planning/critical-path:
    get:
      tags:
        - Planning
      summary: List critical path
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/planning/dependencies:
    get:
      tags:
        - Planning
      summary: List dependencies
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/planning/grooming:
    get:
      tags:
        - Planning
      summary: List grooming
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/planning/grooming/apply:
    post:
      tags:
        - Planning
      summary: Create apply
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/planning/leveling:
    get:
      tags:
        - Planning
      summary: List leveling
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/planning/leveling/apply:
    post:
      tags:
        - Planning
      summary: Create apply
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/planning/scenarios:
    get:
      tags:
        - Planning
      summary: List scenarios
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/planning/scope-creep:
    get:
      tags:
        - Planning
      summary: List scope creep
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/planning/scope-creep/baseline:
    post:
      tags:
        - Planning
      summary: Create baseline
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/planning/sprint-planner:
    get:
      tags:
        - Planning
      summary: List sprint planner
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Planning
      summary: Create sprint planner
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/planning/viable-scope:
    get:
      tags:
        - Planning
      summary: List viable scope
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/planning/viable-scope/apply:
    post:
      tags:
        - Planning
      summary: Create apply
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portfolio:
    get:
      tags:
        - Portfolio
      summary: List portfolio
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/portfolio/budgets:
    get:
      tags:
        - Portfolio
      summary: List budgets
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Portfolio
      summary: Create budget
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portfolio/budgets/entries:
    get:
      tags:
        - Portfolio
      summary: List entries
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Portfolio
      summary: Create a budget entry
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portfolio/forecasts:
    get:
      tags:
        - Portfolio
      summary: List forecasts
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/portfolio/objectives:
    delete:
      tags:
        - Portfolio
      summary: Delete objectives
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Portfolio
      summary: List objectives
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Portfolio
      summary: Update objectives
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Portfolio
      summary: Create objective
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portfolio/programs:
    delete:
      tags:
        - Portfolio
      summary: Delete programs
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Portfolio
      summary: List programs
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Portfolio
      summary: Update programs
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Portfolio
      summary: Create program
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portfolio/resources/allocations:
    delete:
      tags:
        - Portfolio
      summary: Delete allocations
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Portfolio
      summary: List allocations
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Portfolio
      summary: Update allocations
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Portfolio
      summary: Create allocation
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portfolio/resources/capacity:
    get:
      tags:
        - Portfolio
      summary: List capacity
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Portfolio
      summary: Create capacity
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/portfolio/resources/requests:
    get:
      tags:
        - Portfolio
      summary: List requests
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Portfolio
      summary: Update requests
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Portfolio
      summary: Create request
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/project-keys:
    get:
      tags:
        - Projects
      summary: List project keys
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Projects
      summary: Create project key
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
    put:
      tags:
        - Projects
      summary: Update project keys
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/project-templates:
    get:
      tags:
        - Projects
      summary: List project templates
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Projects
      summary: Create project template
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/project-templates/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Projects
      summary: Delete project template
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Projects
      summary: Fetch project template
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - Projects
      summary: Update project template
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/project-templates/{id}/instantiate:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Projects
      summary: Create instantiate
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/projects/{id}/icon:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Projects
      summary: Delete icon
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - Projects
      summary: Create icon
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/projects/{id}/members:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Projects
      summary: Add a member to a project
      description: Admin/owner only. Identify the person by `userId` or by `email`. The user must be an active member of THIS workspace; an email that is not yet a member raises a pending workspace invite as part of the same call, redeemable in-app via /api/invites/mine. Re-adding someone is a no-op and returns 200.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                userId:
                  type: string
                  description: Must be an active member of this workspace.
                email:
                  type: string
                  description: Alternative to userId. Invited if not yet a member.
                team:
                  type: string
      responses:
        "200":
          description: Already a member — unchanged
        "201":
          description: Member added
        "400":
          description: Neither userId nor email supplied
        "401":
          description: Unauthorized
        "402":
          description: The person needs a workspace invite and no seat is free (`plan_limit_reached`)
        "404":
          description: Project not found
          or user not in this workspace: null
    delete:
      tags:
        - Projects
      summary: Remove a member from a project
      description: Admin/owner only. Identify by `email` (the ACL key) or `userId`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                userId:
                  type: string
      responses:
        "200":
          description: Member removed (or was not present)
        "400":
          description: Neither userId nor email supplied
        "404":
          description: Project not found
  /api/projects/{id}/members/{userId}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: userId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Projects
      summary: Delete member
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/projects/{id}/teams:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Projects
      summary: Delete teams
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - Projects
      summary: Create team
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/projects/{id}/visibility:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Projects
      summary: Update visibility
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/projects/archive:
    post:
      tags:
        - Projects
      summary: Create archive
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/projects/budget:
    get:
      tags:
        - Projects
      summary: List budget
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Projects
      summary: Create budget
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/projects/budget/entries:
    delete:
      tags:
        - Projects
      summary: Delete entries
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - Projects
      summary: Create a budget entry
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/projects/duplicate:
    post:
      tags:
        - Projects
      summary: Create duplicate
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/projects/health:
    get:
      tags:
        - Projects
      summary: List health
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - Projects
      summary: Update health
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/projects/phases:
    get:
      tags:
        - Projects
      summary: List phases
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Projects
      summary: Create phase
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/projects/phases/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Projects
      summary: Delete phase
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - Projects
      summary: Update phase
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/projects/roadmap:
    get:
      tags:
        - Projects
      summary: List roadmap
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/access-audit:
    get:
      tags:
        - RBAC
      summary: List access audit
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/audit:
    get:
      tags:
        - RBAC
      summary: List audit
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/default-permissions:
    get:
      tags:
        - RBAC
      summary: List default permissions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - RBAC
      summary: Update default permissions
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/departments:
    get:
      tags:
        - RBAC
      summary: List departments
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - RBAC
      summary: Create department
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/rbac/departments/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - RBAC
      summary: Delete department
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - RBAC
      summary: Update department
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/departments/{id}/members:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - RBAC
      summary: Delete members
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - RBAC
      summary: List members
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - RBAC
      summary: Create member
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/rbac/explorer:
    get:
      tags:
        - RBAC
      summary: List explorer
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/field-permissions:
    delete:
      tags:
        - RBAC
      summary: Delete field permissions
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - RBAC
      summary: List field permissions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - RBAC
      summary: Update field permissions
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/project-overrides:
    delete:
      tags:
        - RBAC
      summary: Delete project overrides
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - RBAC
      summary: List project overrides
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - RBAC
      summary: Update project overrides
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/project-visibility:
    delete:
      tags:
        - RBAC
      summary: Delete project visibility
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - RBAC
      summary: List project visibility
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - RBAC
      summary: Update project visibility
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/role-change-requests:
    get:
      tags:
        - RBAC
      summary: List role change requests
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - RBAC
      summary: Create role change request
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/rbac/role-change-requests/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - RBAC
      summary: Update role change request
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/roles:
    get:
      tags:
        - RBAC
      summary: List roles
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - RBAC
      summary: Create role
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/rbac/roles/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - RBAC
      summary: Delete role
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - RBAC
      summary: Fetch role
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - RBAC
      summary: Update role
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/roles/{id}/duplicate:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - RBAC
      summary: Create duplicate
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/rbac/roles/{id}/preview:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - RBAC
      summary: List preview
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/share-links:
    get:
      tags:
        - RBAC
      summary: List share links
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/share-links/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - RBAC
      summary: Delete share link
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - RBAC
      summary: Update share link
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/sharing-controls:
    get:
      tags:
        - RBAC
      summary: List sharing controls
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - RBAC
      summary: Update sharing controls
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/team-permissions:
    delete:
      tags:
        - RBAC
      summary: Delete team permissions
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - RBAC
      summary: List team permissions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - RBAC
      summary: Update team permissions
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/rbac/time-bound-grants:
    get:
      tags:
        - RBAC
      summary: List time bound grants
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - RBAC
      summary: Create time bound grant
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/rbac/time-bound-grants/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - RBAC
      summary: Delete time bound grant
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/rbac/user-roles:
    get:
      tags:
        - RBAC
      summary: List user roles
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - RBAC
      summary: Update user roles
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/reports:
    get:
      tags:
        - Reports
      summary: List reports
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Reports
      summary: Create report
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/reports/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Reports
      summary: Delete report
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Reports
      summary: Fetch report
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/reports/dashboards:
    get:
      tags:
        - Reports
      summary: List dashboards
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Reports
      summary: Create dashboard
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/reports/dashboards/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Reports
      summary: Delete dashboard
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Reports
      summary: Fetch dashboard
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Reports
      summary: Update dashboard
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/reports/dashboards/{id}/widgets:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Reports
      summary: List widgets
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Reports
      summary: Create widget
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/reports/dashboards/{id}/widgets/{widgetId}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: widgetId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Reports
      summary: Delete widget
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - Reports
      summary: Update widget
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/reports/embed/{token}:
    parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Reports
      summary: Fetch embed
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/reports/export:
    post:
      tags:
        - Reports
      summary: Create export
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/reports/operating:
    get:
      tags:
        - Reports
      summary: Operating metrics report
      parameters:
        - name: days
          in: query
          required: false
          description: Lookback window. Non-numeric or non-positive values fall back to 90.
          schema:
            type: integer
            default: 90
            minimum: 1
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/reports/filters:
    get:
      tags:
        - Reports
      summary: List filters
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Reports
      summary: Create filter
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/reports/filters/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Reports
      summary: Delete filter
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Reports
      summary: Fetch filter
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/reports/generate:
    post:
      tags:
        - Reports
      summary: Create generate
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/reports/insights:
    post:
      tags:
        - Reports
      summary: Create insight
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/reports/insights/narrative:
    post:
      tags:
        - Reports
      summary: Create narrative
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/reports/state-of-business:
    get:
      tags:
        - Reports
      summary: Get the State of the Business report config + status
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Reports
      summary: Configure the State of the Business report (admin)
      description: Create/update the report + schedule and arm the opt-in flag. Admin only.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
  /api/reports/schedules:
    get:
      tags:
        - Reports
      summary: List schedules
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Reports
      summary: Create schedule
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/reports/schedules/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Reports
      summary: Delete schedule
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - Reports
      summary: Update schedule
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/resolutions:
    get:
      tags:
        - Resolutions
      summary: List resolutions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Resolutions
      summary: Create resolution
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/resolutions/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Resolutions
      summary: Delete resolution
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    put:
      tags:
        - Resolutions
      summary: Update resolution
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/scim/v2/Groups:
    get:
      tags:
        - SCIM
      summary: List Groups
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - SCIM
      summary: Create Group
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/scim/v2/Groups/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - SCIM
      summary: Delete Group
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - SCIM
      summary: Fetch Group
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - SCIM
      summary: Update Group
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - SCIM
      summary: Update Group
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/scim/v2/ResourceTypes:
    get:
      tags:
        - SCIM
      summary: List ResourceTypes
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/scim/v2/Schemas:
    get:
      tags:
        - SCIM
      summary: List Schemas
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/scim/v2/ServiceProviderConfig:
    get:
      tags:
        - SCIM
      summary: List ServiceProviderConfig
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/scim/v2/Users:
    get:
      tags:
        - SCIM
      summary: List Users
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - SCIM
      summary: Create User
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/scim/v2/Users/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - SCIM
      summary: Delete User
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - SCIM
      summary: Fetch User
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - SCIM
      summary: Update User
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - SCIM
      summary: Update User
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/settings/billing/balance:
    get:
      tags:
        - Settings
      summary: List balance
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/settings/billing/cancel:
    post:
      tags:
        - Settings
      summary: Create cancel
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/settings/billing/invoices:
    get:
      tags:
        - Settings
      summary: List invoices
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/settings/billing/invoices/{id}/pdf:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Settings
      summary: List pdf
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/settings/billing/order:
    post:
      tags:
        - Settings
      summary: Create order
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/settings/billing/payment-method:
    get:
      tags:
        - Settings
      summary: List payment method
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Settings
      summary: Create payment method
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/settings/billing/profile:
    get:
      tags:
        - Settings
      summary: List profile
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - Settings
      summary: Update profile
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/settings/billing/switch:
    post:
      tags:
        - Settings
      summary: Switch to the Pay As You Go plan
      description: Owner-only. Moves the workspace owner's plan onto the credits-only tier (no subscription, no seat fee, no included credits). Requires a completed credit purchase in the workspace; otherwise answers 402 with code `credits_purchase_required`. Leaving a paid plan this way is prorated like any other downgrade.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - planName
              properties:
                planName:
                  type: string
                  enum:
                    - Pay As You Go Plan
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "402":
          description: A credit purchase is required first
  /api/settings/billing/team:
    post:
      tags:
        - Billing
      summary: Move some of your own credits into the workspace pool
      description: |
        Any workspace member moves credits from their OWN balance into the
        workspace's shared pool; only the caller's wallet is ever debited.
        Funding the pool is the team-billing opt-in, so this also switches
        team billing on (members then spend from the pool first). There is no
        transfer back. `transfer_id` is the idempotency key: a retry with the
        same id replays instead of moving the credits twice. If the pool
        cannot be credited after the debit, the debit is reversed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - amount
                - transfer_id
              properties:
                amount:
                  type: integer
                  minimum: 1
                  description: Whole credits to move
                transfer_id:
                  type: string
                  format: uuid
                  description: Idempotency key
                  one per transfer: null
      responses:
        "200":
          description: "`{ ok: true, amount, deduplicated }`"
        "400":
          description: Invalid amount or transfer_id
        "402":
          description: "Not enough credits — `{ ok: false, error, balance }`"
        "502":
          description: The transfer failed; any debit was reversed
        "503":
          description: Credits are not configured on this server
  /api/settings/billing/tiers:
    get:
      tags:
        - Settings
      summary: List tiers
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/settings/billing/trial:
    get:
      tags:
        - Settings
      summary: List trial
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/settings/billing/webhook:
    post:
      tags:
        - Settings
      summary: Create webhook
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/settings/privacy/data-requests/{requestId}/download:
    parameters:
      - name: requestId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Settings
      summary: Download a completed data-export request
      description: Returns the stored export as a JSON attachment. Only valid for requests of type export that have completed.
      responses:
        "200":
          description: JSON export file
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          description: Request is not a data export
        "401":
          description: Unauthorized
        "404":
          description: Request not found or no workspace
        "409":
          description: Export not ready
        "500":
          description: Stored export could not be read
  /api/settings/privacy:
    get:
      tags:
        - Settings
      summary: List privacy
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Settings
      summary: Create privacy
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/settings/sessions:
    get:
      tags:
        - Settings
      summary: List sessions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/settings/sessions/{sessionId}:
    parameters:
      - name: sessionId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Settings
      summary: Delete session
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/sprints:
    get:
      tags:
        - Sprints
      summary: List sprints
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Sprints
      summary: Create sprint
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/sprints/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Sprints
      summary: Delete sprint
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Sprints
      summary: Fetch sprint
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Sprints
      summary: Update sprint
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/contact:
    post:
      tags:
        - Support
      summary: Create contact
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/app-config/public:
    get:
      tags:
        - System
      summary: List public
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/company-logo:
    get:
      tags:
        - System
      summary: List company logo
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/company-preview:
    get:
      tags:
        - System
      summary: List company preview
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/system/client-error:
    post:
      tags:
        - System
      summary: Create client error
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/system/cron-health:
    get:
      tags:
        - System
      summary: List cron health
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/system/health:
    get:
      tags:
        - System
      summary: List health
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
        "503":
          description: A sub-check failed — database
          SMTP: null
          storage: null
          a stale cron: null
          or native push (APNs unconfigured/unsignable while iOS or macOS device tokens are registered): null
  /api/system/realtime-health:
    get:
      tags:
        - System
      summary: List realtime health
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
        "503":
          description: Realtime stack degraded — VAPID or TURN not configured
          the push census queries failed (DB unreachable / migration not applied): null
          or APNs is unconfigured while native device tokens are registered: null
  /api/system/web-vitals:
    post:
      tags:
        - System
      summary: Create web vital
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/undo:
    get:
      tags:
        - System
      summary: List undo
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - System
      summary: Create undo
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/undo/redo:
    post:
      tags:
        - System
      summary: Create redo
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/uploads/{path}:
    parameters:
      - name: path
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - System
      summary: Fetch upload
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/task-types:
    get:
      tags:
        - Task Types
      summary: List task types
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Task Types
      summary: Create task type
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/task-types/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Task Types
      summary: Delete task type
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    put:
      tags:
        - Task Types
      summary: Update task type
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/tasks/{id}/attachments:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Tasks
      summary: Delete attachments
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Tasks
      summary: List attachments
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Tasks
      summary: Create attachment
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/{id}/comments/agent-reply:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: integer
    post:
      tags:
        - Tasks
      summary: Answer an @agent question in a task comment
      description: |
        Generates the AI agent's answer to a stored comment and appends it to the task in one statement. Idempotent per comment: asking twice returns the existing answer instead of generating a second one. The answer notifies nobody — the model's text is not the caller's words.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - commentId
              properties:
                commentId:
                  type: string
                  description: The comment that mentions @agent.
      responses:
        "200":
          description: This question was already answered
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  alreadyAnswered:
                    type: boolean
                  comment:
                    type: object
                  comments:
                    type: array
                    items:
                      type: object
        "201":
          description: The answer, and the task's comments including it
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  comment:
                    type: object
                  comments:
                    type: array
                    items:
                      type: object
        "400":
          description: That comment does not ask @agent anything
        "401":
          description: Unauthorized
        "403":
          description: The AI agent is disabled for this workspace
        "404":
          description: Task or comment not found
        "429":
          description: Rate limited
          or the daily AI token budget is spent: null
        "502":
          description: The model could not answer
        "503":
          description: The AI agent is not configured
  /api/tasks/{id}/comments:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: integer
    get:
      tags:
        - Tasks
      summary: List a task's comments
      responses:
        "200":
          description: Comments
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  comments:
                    type: array
                    items:
                      $ref: "#/components/schemas/TaskComment"
        "404":
          description: Task not found or not visible to the caller
    post:
      tags:
        - Tasks
      summary: Append a comment to a task
      description: Appends a single comment atomically. Prefer this over PATCHing the task's whole `comments` array, which is a read-modify-write and loses concurrent comments.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                body:
                  type: string
                content:
                  type: string
                  description: Alias for `body`.
      responses:
        "201":
          description: Comment appended
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  comment:
                    $ref: "#/components/schemas/TaskComment"
                  comments:
                    type: array
                    items:
                      $ref: "#/components/schemas/TaskComment"
        "400":
          description: Missing comment body
        "404":
          description: Task not found or not visible to the caller
  /api/tasks/{id}/checklist:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Tasks
      summary: List checklist
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - Tasks
      summary: Update checklist
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/tasks/{id}/estimate:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Tasks
      summary: List estimate
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Tasks
      summary: Create estimate
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/{id}/suggest-split:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Tasks
      summary: Create suggest split
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/{id}/why-stuck:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Tasks
      summary: Create why stuck
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/activity:
    get:
      tags:
        - Tasks
      summary: List activity
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/tasks/approvals:
    get:
      tags:
        - Tasks
      summary: List approvals
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Tasks
      summary: Create approval
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/approvals/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Tasks
      summary: Update approval
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/tasks/batch:
    get:
      tags:
        - Tasks
      summary: List batch
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/tasks/bulk:
    patch:
      tags:
        - Tasks
      summary: Update bulk
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/tasks/dependencies:
    get:
      tags:
        - Tasks
      summary: List dependencies
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Tasks
      summary: Create a task dependency
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/dependencies/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Tasks
      summary: Delete a task dependency
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/tasks/effort:
    patch:
      tags:
        - Tasks
      summary: Update effort
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/tasks/gantt:
    get:
      tags:
        - Tasks
      summary: List gantt
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/tasks/intelligence/decompose:
    post:
      tags:
        - Tasks
      summary: Create decompose
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/intelligence/entity-links:
    post:
      tags:
        - Tasks
      summary: Create entity link
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/intelligence/eta-confidence:
    post:
      tags:
        - Tasks
      summary: Create eta confidence
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/intelligence/find-duplicates:
    post:
      tags:
        - Tasks
      summary: Create find duplicate
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/intelligence/parse:
    post:
      tags:
        - Tasks
      summary: Create parse
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/intelligence/skills:
    post:
      tags:
        - Tasks
      summary: Create skill
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/intelligence/slip-risk:
    post:
      tags:
        - Tasks
      summary: Create slip risk
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/intelligence/templates:
    get:
      tags:
        - Tasks
      summary: List templates
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Tasks
      summary: Create template
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/mentions:
    get:
      tags:
        - Tasks
      summary: List mentions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Tasks
      summary: Update mentions
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Tasks
      summary: Create mention
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/milestones:
    get:
      tags:
        - Tasks
      summary: List milestones
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Tasks
      summary: Create milestone
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/milestones/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Tasks
      summary: Delete milestone
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - Tasks
      summary: Update milestone
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/tasks/subscriptions:
    delete:
      tags:
        - Tasks
      summary: Delete subscriptions
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    get:
      tags:
        - Tasks
      summary: List subscriptions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Tasks
      summary: Create subscription
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/tasks/views:
    get:
      tags:
        - Tasks
      summary: List views
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/time-productivity/capture:
    get:
      tags:
        - Time Productivity
      summary: List capture
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Time Productivity
      summary: Create capture
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/time-productivity/focus:
    get:
      tags:
        - Time Productivity
      summary: Get focus session state
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Time Productivity
      summary: Start a focus session
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/time-productivity/insights:
    get:
      tags:
        - Time Productivity
      summary: List insights
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/time-productivity/plan:
    get:
      tags:
        - Time Productivity
      summary: List plan
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Time Productivity
      summary: Update plan
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Time Productivity
      summary: Create plan
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "409":
          description: action=stop-task found no running timer for this member
  /api/versions:
    get:
      tags:
        - Versions
      summary: List versions
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Versions
      summary: Create version
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/versions/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Versions
      summary: Delete version
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    put:
      tags:
        - Versions
      summary: Update version
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/workspace:
    delete:
      tags:
        - Workspace
      summary: Delete workspace
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /api/workspace/create:
    post:
      tags:
        - Workspace
      summary: Create a new workspace
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/workspace/guests:
    get:
      tags:
        - Workspace
      summary: List guests
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Workspace
      summary: Create guest
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/workspace/guests/{userId}:
    parameters:
      - name: userId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Workspace
      summary: Delete guest
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - Workspace
      summary: Update guest
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/workspace/invites:
    get:
      tags:
        - Workspace
      summary: List invites
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Workspace
      summary: Create invite
      description: |
        Optional `workspaceId` in the body names the workspace the calling page was loaded from; a mismatch with the session's workspace is refused with 409 `bundle_workspace_mismatch`.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "409":
          description: Already a member, or `bundle_workspace_mismatch`
  /api/workspace/invites/{inviteId}:
    parameters:
      - name: inviteId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Workspace
      summary: Delete invite
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    patch:
      tags:
        - Workspace
      summary: Update invite
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/workspace/invites/{inviteId}/resend:
    parameters:
      - name: inviteId
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Workspace
      summary: Create resend
      parameters:
        - name: workspaceId
          in: query
          required: false
          schema:
            type: string
          description: The workspace the calling page was loaded from; a mismatch is refused with 409 `bundle_workspace_mismatch`.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "402":
          description: Resending an expired or declined invite takes a seat back, and none is free (`plan_limit_reached`)
        "409":
          description: Already accepted, or `bundle_workspace_mismatch`
  /api/workspace/invites/{inviteId}/approve:
    parameters:
      - name: inviteId
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Workspace
      summary: Approve a member-raised invite request
      description: |
        Admin-only. Approves an invite with status `requested` (created by a member when the workspace `invite_policy` is `members-with-approval`), rotating its token and expiry and sending the invite email. Returns 409 if the invite is not in the `requested` state.
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Not found
        "409":
          description: Invite is not awaiting approval
  /api/workspace/invites/bulk:
    post:
      tags:
        - Workspace
      summary: Create bulk
      description: "Admin only. Invites up to 50 `emails` at one `role`. Seats are checked for the whole batch before anything is sent: pending invites hold a seat, people already in the workspace or already invited are skipped and use none, and a batch that does not fit is refused with 402 and nothing is sent."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "402":
          description: Not enough seats for the batch (`plan_limit_reached`, with `limit`, `used`, `delta`). Nothing was sent.
  /api/workspace/invites/csv:
    post:
      tags:
        - Workspace
      summary: Create csv
      description: "Admin only. Invites up to 200 people from a `csv` string (header `email`, optional `name`, `role`, `projects`) or parsed `rows` (`email`, `name`, `role`, `project_refs`, `team`). Seats are checked for the whole batch before anything is sent: pending invites hold a seat, people already in the workspace or already invited are skipped and use none, and a batch that does not fit is refused with 402 and nothing is sent. `project_refs` (project ids, or names in a CSV) are granted by email, so each person has those projects when they accept; a ref that names no project fails its row. When projects were granted, `projectAccess` carries the bundle `previousVersion` and `version`, so a page holding the bundle can adopt the new version."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "402":
          description: Not enough seats for the batch (`plan_limit_reached`, with `limit`, `used`, `delta`). Nothing was sent.
  /api/workspace/list:
    get:
      tags:
        - Workspace
      summary: List the caller's workspaces
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/workspace/members:
    get:
      tags:
        - Workspace
      summary: List members
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Workspace
      summary: Create member
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/workspace/members/{userId}:
    parameters:
      - name: userId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Workspace
      summary: Delete member
      description: |
        Removes the membership and ends the member's sessions. Refuses removing yourself, the last owner or the last admin; removing an owner requires owner. Optional `workspaceId` names the workspace the calling page was loaded from; a mismatch is refused with 409 `bundle_workspace_mismatch`.
      parameters:
        - name: workspaceId
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Deleted
        "400":
          description: Cannot remove yourself
        "403":
          description: Removing an owner requires owner
        "404":
          description: Not found
        "409":
          description: Last owner/admin, or `bundle_workspace_mismatch`
    patch:
      tags:
        - Workspace
      summary: Update member
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/workspace/members/{userId}/deprovision:
    parameters:
      - name: userId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Workspace
      summary: List deprovision
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Workspace
      summary: Create deprovision
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/workspace/members/{userId}/mfa-reset:
    parameters:
      - name: userId
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Workspace
      summary: Reset a member's two-factor authentication
      description: |
        Clears a member's authenticator, passkeys and recovery codes so they can enrol a new device — the lost-device path. Admin-only; resetting another admin or the owner requires owner. Never self-service. Audited, and the affected member is emailed.
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
          self-reset: null
          or member has no MFA: null
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Member not found in this workspace
  /api/workspace/members/{userId}/transfer:
    parameters:
      - name: userId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Workspace
      summary: List transfer
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    post:
      tags:
        - Workspace
      summary: Create transfer
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/workspace/recording-policy:
    get:
      tags:
        - Workspace
      summary: List recording policy
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    patch:
      tags:
        - Workspace
      summary: Update recording policy
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/workspace/seats:
    get:
      tags:
        - Workspace
      summary: List seats
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - Workspace
      summary: Update seats
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/workspace/seats/named/{userId}:
    parameters:
      - name: userId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags:
        - Workspace
      summary: Remove a named seat from a member
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
    post:
      tags:
        - Workspace
      summary: Assign a named seat to a member
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/workspace/switch:
    post:
      tags:
        - Workspace
      summary: Switch the active workspace (session callers only)
      description: |
        Sets the browser's active-workspace cookie after validating membership. API-key callers get 409 — keys are bound to one workspace at mint time and cannot switch; mint a key in the target workspace instead.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - workspaceId
              properties:
                workspaceId:
                  type: string
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
        "403":
          description: Not a member of that workspace
        "409":
          description: Caller authenticated with a workspace-bound API key
  /api/ediscovery/export:
    post:
      tags:
        - eDiscovery
      summary: Create export
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/ediscovery/search:
    post:
      tags:
        - eDiscovery
      summary: Create search
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "401":
          description: Unauthorized
  /api/github/app/install:
    get:
      tags:
        - Integrations
      summary: Start the GitHub App install flow (302 redirects to github.com)
      responses:
        "302":
          description: Redirect to GitHub install URL
        "401":
          description: Unauthorized
  /api/github/app/callback:
    get:
      tags:
        - Integrations
      summary: GitHub App install callback (handles installation_id, redirects back to app)
      parameters:
        - name: installation_id
          in: query
          schema:
            type: string
        - name: setup_action
          in: query
          schema:
            type: string
        - name: state
          in: query
          schema:
            type: string
      responses:
        "302":
          description: Redirect back to the integrations UI
        "401":
          description: Unauthorized
  /api/github/oauth/start:
    get:
      tags:
        - Integrations
      summary: Start the GitHub OAuth sign-in flow (302 redirects to github.com)
      responses:
        "302":
          description: Redirect to GitHub authorize URL
        "401":
          description: Unauthorized
  /api/github/repos:
    get:
      tags:
        - Integrations
      summary: List GitHub repositories available to connect
      responses:
        "200":
          description: Repository list
        "401":
          description: Unauthorized
        "412":
          description: GitHub not connected
  /api/github/issue-sync:
    get:
      tags:
        - Integrations
      summary: List connected GitHub repos and their issue-sync opt-in state
      responses:
        "200":
          description: Repos with issueSyncEnabled
        "401":
          description: Unauthorized
        "403":
          description: Admin access required
    patch:
      tags:
        - Integrations
      summary: Enable or disable GitHub Issue sync for one repository
      description: |
        Turns two-way sync between a repo's GitHub Issues and Kavanah tasks on or off. Defaults to off. While enabled, `issues` webhook events create and update tasks, and task status changes close or reopen the linked issue. Admin only.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - repoExternalId
                - enabled
              properties:
                repoExternalId:
                  type: string
                enabled:
                  type: boolean
      responses:
        "200":
          description: Updated repo
        "400":
          description: Invalid body
        "401":
          description: Unauthorized
        "403":
          description: Admin access required
        "404":
          description: Repo not found
  /api/github/status:
    get:
      tags:
        - Integrations
      summary: Get the workspace GitHub connection status
      responses:
        "200":
          description: Connection status
        "401":
          description: Unauthorized
  /api/github/sync:
    post:
      tags:
        - Integrations
      summary: Trigger a GitHub sync for connected repositories
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                repoId:
                  type: string
      responses:
        "200":
          description: Sync started or completed
        "401":
          description: Unauthorized
        "412":
          description: GitHub not connected
  /api/github/webhook:
    post:
      tags:
        - Integrations
      summary: Receive GitHub webhook deliveries (signature-verified, unauthenticated)
      description: Inbound GitHub webhook. Verified via the `X-Hub-Signature-256` header rather than a session.
      responses:
        "200":
          description: Event accepted
        "202":
          description: Event ignored
        "401":
          description: Invalid signature
  /api/requests:
    get:
      tags:
        - Requests
      summary: List intake requests for the workspace (members see their own; admins see all)
      responses:
        "200":
          description: Requests list
        "400":
          description: No active workspace
    patch:
      tags:
        - Requests
      summary: Update the status of an intake request
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
                - status
              properties:
                id:
                  type: string
                status:
                  type: string
      responses:
        "200":
          description: Request updated
        "400":
          description: Missing id or invalid status
        "403":
          description: Forbidden
        "404":
          description: Not found
    post:
      tags:
        - Requests
      summary: Convert an intake request into a task
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
                - projectRef
              properties:
                id:
                  type: string
                projectRef:
                  type: string
                assignedTo:
                  type: string
                  nullable: true
      responses:
        "200":
          description: Task created from request
        "400":
          description: Missing id or projectRef
        "403":
          description: Forbidden
        "404":
          description: Not found
        "409":
          description: Request already converted
  /api/requests/count:
    get:
      tags:
        - Requests
      summary: Get the count of open intake requests (for nav badges)
      responses:
        "200":
          description: Request count
  /api/requests/pages:
    get:
      tags:
        - Requests
      summary: List request intake links (pages)
      responses:
        "200":
          description: Request pages
    post:
      tags:
        - Requests
      summary: Create a request intake link
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
              properties:
                title:
                  type: string
                description:
                  type: string
                accessMode:
                  type: string
                  enum:
                    - public
                    - clients_only
                scope:
                  type: string
                  enum:
                    - user
                    - workspace
      responses:
        "200":
          description: Page created
        "400":
          description: Invalid body / title
        "403":
          description: Only admins can create workspace-level links
    patch:
      tags:
        - Requests
      summary: Update a request intake link
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
              properties:
                id:
                  type: string
                title:
                  type: string
                description:
                  type: string
                  nullable: true
                accessMode:
                  type: string
                  enum:
                    - public
                    - clients_only
                isActive:
                  type: boolean
      responses:
        "200":
          description: Page updated
        "400":
          description: Missing id / invalid field
        "403":
          description: Forbidden
        "404":
          description: Not found
    delete:
      tags:
        - Requests
      summary: Delete a request intake link
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Page deleted
        "400":
          description: Missing id
        "403":
          description: Forbidden
        "404":
          description: Not found
  /api/requests/pages/{id}/fields:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Requests
      summary: List the custom fields for a request intake link
      responses:
        "200":
          description: Fields list
        "404":
          description: Not found
    put:
      tags:
        - Requests
      summary: Replace the custom fields for a request intake link
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - fields
              properties:
                fields:
                  type: array
                  items:
                    type: object
      responses:
        "200":
          description: Fields replaced
        "400":
          description: Invalid fields
        "403":
          description: Forbidden
        "404":
          description: Not found
  /api/r/verify:
    get:
      tags:
        - Requests
      summary: Confirm a public request submission via emailed token (returns an HTML page)
      description: Public, unauthenticated. Consumes a one-time verification token and finalizes the submission.
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: HTML confirmation page (success
          expired: null
          or invalid): null
  /api/r/{token}/submit:
    parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Requests
      summary: Submit a public intake request against a link token
      description: Public, unauthenticated (rate limited). Accepts JSON or multipart form data with `subject`, `body`, requester identity, and any custom fields. Links in `clients_only` mode require a portal session.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - subject
                - body
              properties:
                subject:
                  type: string
                body:
                  type: string
                name:
                  type: string
                email:
                  type: string
          multipart/form-data:
            schema:
              type: object
      responses:
        "200":
          description: Submission accepted (may require email verification)
        "400":
          description: Invalid submission
        "404":
          description: Link no longer available
        "429":
          description: Rate limited
  /api/people/skills:
    get:
      tags:
        - People
      summary: List declared/learned skill profiles for workspace members
      parameters:
        - name: member
          in: query
          schema:
            type: string
          description: Filter to a single member by display name.
      responses:
        "200":
          description: Skill profiles
        "401":
          description: Unauthorized
    put:
      tags:
        - People
      summary: Replace a member's declared skill set
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - memberName
                - skills
              properties:
                memberName:
                  type: string
                skills:
                  type: array
                  items:
                    type: object
                    required:
                      - skill
                      - strength
                    properties:
                      skill:
                        type: string
                        description: A canonical vocabulary key, or the member's own custom free-text skill.
                      strength:
                        type: number
                        minimum: 0
                        maximum: 1
                        description: Declared baseline strength; reinforcement may raise the stored value later.
      responses:
        "200":
          description: Skills updated
        "400":
          description: Invalid body
        "401":
          description: Unauthorized
  /api/portfolio/availability/requests:
    get:
      tags:
        - Portfolio
      summary: List availability requests (leader-to-team asks)
      responses:
        "200":
          description: Availability requests
        "400":
          description: No active workspace
    post:
      tags:
        - Portfolio
      summary: Create an availability request (admin only)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
                - periodStart
                - periodEnd
              properties:
                projectRef:
                  type: string
                  nullable: true
                  description: null = workspace-wide
                title:
                  type: string
                message:
                  type: string
                periodStart:
                  type: string
                  format: date
                periodEnd:
                  type: string
                  format: date
                dueDate:
                  type: string
                  format: date
                  nullable: true
      responses:
        "200":
          description: Request created
        "400":
          description: Invalid body
        "403":
          description: Admin only
    patch:
      tags:
        - Portfolio
      summary: Update an availability request status (admin only)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
              properties:
                id:
                  type: string
                status:
                  type: string
                  enum:
                    - open
                    - closed
      responses:
        "200":
          description: Request updated
        "400":
          description: Invalid body
        "403":
          description: Admin only
        "404":
          description: Not found
    delete:
      tags:
        - Portfolio
      summary: Delete an availability request (admin only)
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Request deleted
        "400":
          description: Missing id
        "403":
          description: Admin only
        "404":
          description: Not found
  /api/portfolio/availability/submissions:
    get:
      tags:
        - Portfolio
      summary: List availability submissions (admins see all; members see their own)
      parameters:
        - name: requestId
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Availability submissions
        "400":
          description: No active workspace
    post:
      tags:
        - Portfolio
      summary: Upsert the caller's own availability submission
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                requestId:
                  type: string
                  nullable: true
                  description: Omit for a standing entry
                status:
                  type: string
                  enum:
                    - available
                    - limited
                    - unavailable
                availableHoursPerWeek:
                  type: number
                periodStart:
                  type: string
                  format: date
                  nullable: true
                periodEnd:
                  type: string
                  format: date
                  nullable: true
                timeOff:
                  type: array
                  items:
                    type: object
                    properties:
                      start:
                        type: string
                      end:
                        type: string
                      note:
                        type: string
                notes:
                  type: string
      responses:
        "200":
          description: Submission saved
        "400":
          description: Invalid body
  /api/ai-agent/org-templates:
    get:
      tags:
        - AI Agent
      summary: List org templates (whole AI organisations with standing meetings)
      description: "Static catalogue: each template's roles (tier, job description, scopes, reporting line, North Star contribution) and meeting series. Member-readable."
      responses:
        "200":
          description: Templates
  /api/ai-agent/org-templates/{key}/preview:
    post:
      tags:
        - AI Agent
      summary: Preview applying an org template in this workspace
      description: Admin only. Writes nothing. Returns which roles adopt an existing AI employee (matched by template stamp, role title or name; never duplicated), which are created, which reporting lines are filled in, which meeting series are created (disarmed), and each series' projected monthly credits if armed.
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrgTemplateSelection"
      responses:
        "200":
          description: Preview
        "400":
          description: Invalid selection
        "403":
          description: Admins only
        "404":
          description: Unknown template
  /api/ai-agent/org-templates/{key}/apply:
    post:
      tags:
        - AI Agent
      summary: Apply an org template
      description: "Admin only. Idempotent: creates only missing AI employees and meeting series; existing employees are adopted unchanged (a manager is filled in only where none is set); never deletes. Every series is created disarmed."
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrgTemplateSelection"
      responses:
        "200":
          description: Applied
        "400":
          description: Invalid selection
        "403":
          description: Admins only
        "404":
          description: Unknown template
  /api/ai-agent/personas:
    get:
      tags:
        - AI Agent
      summary: List AI agent personas (AI employees)
      responses:
        "200":
          description: Personas
    post:
      tags:
        - AI Agent
      summary: Create an AI agent persona
      description: Admin only. Omitting allowed_scopes creates the employee with the starter set (tasks_projects, communications, reports_analytics, knowledge_files), the same default as the roster editor and the create_ai_persona agent tool. Pass null or an empty array for an unrestricted employee.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  maxLength: 80
                role_title:
                  type: string
                  maxLength: 120
                personality:
                  type: string
                  maxLength: 8000
                  description: The employee's instructions and personality (its system-prompt layer).
                model:
                  type: string
                  description: Model id from available_models; null or empty uses the workspace default.
                allowed_scopes:
                  type: array
                  nullable: true
                  items:
                    type: string
                    enum:
                      - tasks_projects
                      - communications
                      - email_calendar
                      - dev_github
                      - workflows_automations
                      - reports_analytics
                      - crm
                      - finance
                      - knowledge_files
                      - admin
                  description: Capability groups the employee may use. Omitted gives the starter set; null or [] means every capability.
                member_user_ids:
                  type: array
                  nullable: true
                  items:
                    type: string
                    format: uuid
                  description: Who may chat with the employee. Omitted, null or [] leaves it open to every member.
                avatar_url:
                  type: string
                avatar_color:
                  type: string
                  maxLength: 40
                avatar_prompt:
                  type: string
                  maxLength: 2000
                voice_id:
                  type: string
                enabled:
                  type: boolean
                is_cross_workspace:
                  type: boolean
                kvn_know_how:
                  type: string
                  maxLength: 8000
                kvn_vision:
                  type: string
                  maxLength: 8000
                kvn_negation:
                  type: string
                  maxLength: 8000
                security_guardrails:
                  type: string
                  maxLength: 8000
      responses:
        "201":
          description: Persona created
        "400":
          description: Invalid input
        "403":
          description: AI agent disabled
        "500":
          description: Persona created but its member access list could not be applied
  /api/ai-agent/personas/generate:
    post:
      tags:
        - AI Agent
      summary: Draft a persona from a natural-language description
      description: Admin/owner only. Returns a generated (unsaved) persona.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - prompt
              properties:
                prompt:
                  type: string
                  maxLength: 2000
                  description: Description of the AI employee. Truncated at 2000 chars.
      responses:
        "200":
          description: Persona draft generated
        "400":
          description: Prompt missing or empty
        "401":
          description: Unauthorized
        "403":
          description: AI agent disabled
          or admin/owner required: null
  /api/ai-agent/personas/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - AI Agent
      summary: Update an AI agent persona
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                roleTitle:
                  type: string
                model:
                  type: string
                systemPrompt:
                  type: string
                avatar:
                  type: string
      responses:
        "200":
          description: Persona updated
        "400":
          description: Invalid input
        "403":
          description: AI agent disabled
        "404":
          description: Not found
    delete:
      tags:
        - AI Agent
      summary: Delete an AI agent persona
      responses:
        "200":
          description: Persona deleted
        "403":
          description: AI agent disabled
        "404":
          description: Not found
  /api/me/notes:
    get:
      tags:
        - Personal
      summary: Get the caller's scratch note
      responses:
        "200":
          description: The note (may be null if never set)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  note:
                    nullable: true
    put:
      tags:
        - Personal
      summary: Upsert the caller's scratch note
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - body
              properties:
                body:
                  type: string
                  description: Note body (capped, 413 if too large).
      responses:
        "200":
          description: Note saved
        "400":
          description: Invalid body
        "413":
          description: Note exceeds the size cap
  /api/me/reminders:
    get:
      tags:
        - Personal
      summary: List the caller's reminders
      responses:
        "200":
          description: Reminders
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  reminders:
                    type: array
                    items:
                      type: object
    post:
      tags:
        - Personal
      summary: Create a reminder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - text
                - remindAt
              properties:
                text:
                  type: string
                remindAt:
                  type: string
                  format: date-time
                taskId:
                  type: integer
                  nullable: true
                  description: Optional task in the same workspace.
      responses:
        "200":
          description: Reminder created
        "400":
          description: Invalid input
  /api/me/reminders/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Personal
      summary: Update a reminder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                text:
                  type: string
                remindAt:
                  type: string
                  format: date-time
                done:
                  type: boolean
                taskId:
                  type: integer
                  nullable: true
      responses:
        "200":
          description: Reminder updated
        "400":
          description: Invalid input
        "404":
          description: Reminder not found
    delete:
      tags:
        - Personal
      summary: Delete a reminder
      responses:
        "200":
          description: Reminder deleted
        "404":
          description: Reminder not found
  /api/learn/progress:
    get:
      tags:
        - Personal
      summary: The caller's Learn progress — completions, listening, reading
      description: "Per-person, not per-workspace — the same document is returned in every workspace the caller belongs to. `completed` keeps its original shape (course slug → completed lesson slugs, oldest first) for older clients. `items` holds per-item state keyed by course slug then item slug, where an item is a lesson OR the course introduction narration (its `narrationIntroSlug`). `courses` are rollups over PUBLISHED lessons only; `continue` is the most recently touched unfinished lesson (or the next lesson of a started course), or null. `Cache-Control: private, no-store` — the same client writes this and re-reads it straight after."
      responses:
        "200":
          description: Learn progress
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  completed:
                    type: object
                    additionalProperties:
                      type: array
                      items:
                        type: string
                  items:
                    type: object
                    description: course slug → item slug → LearnItemProgress
                    additionalProperties:
                      type: object
                      additionalProperties:
                        $ref: "#/components/schemas/LearnItemProgress"
                  courses:
                    type: object
                    description: course slug → rollup
                    additionalProperties:
                      type: object
                      properties:
                        completed:
                          type: integer
                        total:
                          type: integer
                          description: Published lessons
                        percent:
                          type: integer
                          description: Completed share, 0–100, floored
                        inProgress:
                          type: integer
                        listenedPercent:
                          type: integer
                          description: Mean listened share across published lessons, floored
                        introListenedPercent:
                          type:
                            - integer
                            - "null"
                          description: null when the course has no introduction
                  continue:
                    type:
                      - object
                      - "null"
                    properties:
                      courseSlug:
                        type: string
                      lessonSlug:
                        type: string
                      mode:
                        type: string
                        enum:
                          - listen
                          - read
                          - start
                      positionSeconds:
                        type: number
                      readSection:
                        type:
                          - integer
                          - "null"
                      listenedPercent:
                        type: number
                      updatedAt:
                        type:
                          - string
                          - "null"
                        format: date-time
    put:
      tags:
        - Personal
      summary: Mark a Learn lesson complete or not complete
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - course
                - lesson
                - completed
              properties:
                course:
                  type: string
                  description: Course slug
                lesson:
                  type: string
                  description: Lesson slug within that course
                completed:
                  type: boolean
      responses:
        "200":
          description: Progress saved (idempotent)
        "400":
          description: Invalid input
        "404":
          description: Unknown course or lesson
        "409":
          description: The lesson is not published yet
    patch:
      tags:
        - Personal
      summary: Save a Learn listening or reading checkpoint
      description: "Records where the caller is in a lesson's (or course introduction's) narration, how far they have listened, and/or the furthest body section they have read. Merge rules: the position is last-write-wins by `clientUpdatedAt` (clamped to server time), so a late, queued save from another device cannot rewind a newer position; the listened share and `readSection` only ever grow. `furthestSeconds` is an offset into one `narrationVersion` — a newer write in a new version replaces it. The first checkpoint that takes a LESSON's listened share to 95% or more marks it complete (`autoCompleted: true` in the answer) — once; a lesson the caller later un-completes is never re-completed automatically."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - course
                - lesson
              properties:
                course:
                  type: string
                  description: Course slug
                lesson:
                  type: string
                  description: Lesson slug, or the course's narrationIntroSlug
                narrationVersion:
                  type: string
                  description: The narration content version the offsets refer to
                positionSeconds:
                  type: number
                  minimum: 0
                  maximum: 21600
                furthestSeconds:
                  type: number
                  minimum: 0
                  maximum: 21600
                  description: Furthest point reached by contiguous playback
                durationSeconds:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 21600
                readSection:
                  type: integer
                  minimum: 0
                  maximum: 1000
                  description: Furthest body section reached (1…n); n+1 = the end of the lesson
                readSectionsTotal:
                  type: integer
                  minimum: 0
                  maximum: 1000
                clientUpdatedAt:
                  type: string
                  format: date-time
                  description: When the client captured this checkpoint
      responses:
        "200":
          description: Checkpoint merged
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  course:
                    type: string
                  lesson:
                    type: string
                  autoCompleted:
                    type: boolean
                  progress:
                    $ref: "#/components/schemas/LearnItemProgress"
        "400":
          description: Invalid input (needs positionSeconds/furthestSeconds or readSection; bounded non-negative numbers)
        "404":
          description: Unknown course or lesson
        "409":
          description: The lesson is not published yet
  /api/learn/courses:
    get:
      tags:
        - Personal
      summary: The Learn course catalogue
      description: 'Every course in the catalogue with its modules and lesson outlines, minus lesson bodies. `published` is false for a lesson that has no body yet ("coming soon"). Same catalogue the web reader renders. Build-static, so it carries `Cache-Control: private, max-age=0, stale-while-revalidate=60`.'
      responses:
        "200":
          description: Courses
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  courses:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                      required:
                        - slug
                        - title
                        - subtitle
                        - tagline
                        - blurb
                        - audience
                        - level
                        - modules
                        - lessons
                      properties:
                        slug:
                          type: string
                        title:
                          type: string
                        subtitle:
                          type: string
                        tagline:
                          type: string
                        blurb:
                          type: string
                        audience:
                          type: string
                        level:
                          type: string
                        image:
                          type: string
                          description: Public cover path, e.g. /course/<slug>.jpg
                        modules:
                          type: array
                          items:
                            type: object
                            additionalProperties: true
                            properties:
                              id:
                                type: string
                              number:
                                type: integer
                              title:
                                type: string
                              subtitle:
                                type: string
                              blurb:
                                type: string
                        lessons:
                          type: array
                          items:
                            type: object
                            additionalProperties: true
                            properties:
                              slug:
                                type: string
                              number:
                                type: string
                              module:
                                type: string
                              title:
                                type: string
                              subtitle:
                                type: string
                              estimatedMinutes:
                                type: integer
                              outline:
                                type: object
                                properties:
                                  abstract:
                                    type: string
                                  learningObjectives:
                                    type: array
                                    items:
                                      type: string
                              published:
                                type: boolean
                        introNarration:
                          type:
                            - object
                            - "null"
                          description: Narration metadata for the course introduction, played via `/api/tts?kind=course&slug={narrationIntroSlug}&v={version}`. null when the course has no introduction.
                          properties:
                            version:
                              type: string
                            estimatedSeconds:
                              type: integer
  /api/learn/courses/{course}/lessons/{lesson}:
    get:
      tags:
        - Personal
      summary: One Learn lesson, with its narration version
      description: The lesson's full content. `narration.version` is the `v` parameter `/api/tts?kind=course&slug={lesson}` needs to serve cached audio; null when the lesson is unpublished.
      parameters:
        - name: course
          in: path
          required: true
          schema:
            type: string
        - name: lesson
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The lesson
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  course:
                    type: object
                    properties:
                      slug:
                        type: string
                      title:
                        type: string
                  lesson:
                    type: object
                    additionalProperties: true
                  narration:
                    type: object
                    nullable: true
                    properties:
                      version:
                        type: string
                      estimatedSeconds:
                        type: number
        "404":
          description: Unknown course or lesson
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
  /api/me/todos:
    get:
      tags:
        - Personal
      summary: List the caller's to-dos
      responses:
        "200":
          description: To-dos
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  todos:
                    type: array
                    items:
                      type: object
    post:
      tags:
        - Personal
      summary: Create a to-do
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - text
              properties:
                text:
                  type: string
      responses:
        "200":
          description: To-do created
        "400":
          description: Invalid input
  /api/me/todos/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Personal
      summary: Update a to-do
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                text:
                  type: string
                done:
                  type: boolean
                sortOrder:
                  type: integer
      responses:
        "200":
          description: To-do updated
        "400":
          description: Invalid input
        "404":
          description: To-do not found
    delete:
      tags:
        - Personal
      summary: Delete a to-do
      responses:
        "200":
          description: To-do deleted
        "404":
          description: To-do not found
  /api/manager:
    get:
      tags:
        - Manager
      summary: The caller's Manager Mode state (agreement, scorecard, commitments, check-ins)
      description: |
        Never 404s. A user who has never enabled Manager Mode gets a disabled default agreement, so callers need no null branch.
      responses:
        "200":
          description: Manager state
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  agreement:
                    type: object
                  scorecard:
                    type: object
                  openCommitments:
                    type: array
                    items:
                      type: object
                  recentCommitments:
                    type: array
                    items:
                      type: object
                  checkins:
                    type: array
                    items:
                      type: object
    patch:
      tags:
        - Manager
      summary: Update the caller's Manager Mode agreement
      description: |
        PARTIAL — only fields present in the body are written. `enabled` is the opt-in for the whole feature. `rewardsEnabled` and a non-zero `monthlyCreditCap` are BOTH required before any credit payout is possible; setting `rewardsEnabled` to false also resets the cap to 0.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled:
                  type: boolean
                style:
                  type: string
                  enum:
                    - supportive
                    - balanced
                    - demanding
                objectives:
                  type: string
                workDays:
                  type: array
                  items:
                    type: integer
                    minimum: 0
                    maximum: 6
                  description: 0=Sunday … 6=Saturday
                hoursPerDay:
                  type: number
                  description: Focused hours on a work day
                dayEndHour:
                  type: integer
                  minimum: 1
                  maximum: 23
                confidence:
                  type: string
                  enum:
                    - p50
                    - p90
                maxOpenCommitments:
                  type: integer
                  minimum: 1
                  maximum: 50
                checkinFrequency:
                  type: string
                  enum:
                    - off
                    - daily
                    - weekdays
                    - weekly
                checkinHour:
                  type: integer
                  minimum: 0
                  maximum: 23
                checkinDayOfWeek:
                  type: integer
                  minimum: 0
                  maximum: 6
                timezone:
                  type: string
                  description: IANA zone
                rewardsEnabled:
                  type: boolean
                pointsPerCredit:
                  type: integer
                  minimum: 1
                monthlyCreditCap:
                  type: integer
                  minimum: 0
      responses:
        "200":
          description: Agreement updated
        "400":
          description: No recognised fields to update
  /api/manager/plan:
    get:
      tags:
        - Manager
      summary: Preview an achievable deadline without committing to it
      description: |
        Runs the same computation `POST /api/manager/commitments` uses: the estimate is uplifted by the caller's own estimate-vs-actual percentile, queued behind work they have already promised, and spread across their real working days and focused hours.
      parameters:
        - name: estimateHours
          in: query
          required: false
          schema:
            type: number
      responses:
        "200":
          description: Deadline forecast
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  dueAt:
                    type: string
                    format: date-time
                  dueDate:
                    type: string
                  timezone:
                    type: string
                  calibratedHours:
                    type:
                      - number
                      - "null"
                  upliftMultiplier:
                    type: number
                  workDaysUsed:
                    type: integer
                  queuedHours:
                    type: number
                  openCommitments:
                    type: integer
                  atCapacity:
                    type: boolean
                  horizonExceeded:
                    type: boolean
                  explanation:
                    type: string
        "400":
          description: estimateHours is not a number
  /api/manager/commitments:
    get:
      tags:
        - Manager
      summary: List the caller's own commitments
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum:
              - open
              - met
              - missed
              - cancelled
              - all
        - name: limit
          in: query
          schema:
            type: integer
            maximum: 200
      responses:
        "200":
          description: Commitments
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  commitments:
                    type: array
                    items:
                      type: object
    post:
      tags:
        - Manager
      summary: Commit to a piece of work by a deadline
      description: |
        The deadline is computed from the caller's calibration and queue unless `dueAt` is supplied. It is frozen for scoring at creation — renegotiating later moves the working date but never the date the commitment is graded against. Does not create tasks; task creation from a commitment goes through the KVN-gated `commit_to_work` agent tool.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
              properties:
                title:
                  type: string
                estimateHours:
                  type: number
                notes:
                  type: string
                dueAt:
                  type: string
                  format: date-time
                taskId:
                  type: integer
                  description: Must belong to the caller's workspace; verified before use.
      responses:
        "200":
          description: Commitment recorded
        "400":
          description: Invalid input
        "404":
          description: Task not found in this workspace
        "409":
          description: Manager Mode disabled
          at the open-commitment ceiling: null
          or the task already has an open commitment: null
  /api/manager/commitments/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - Manager
      summary: Renegotiate, complete, or cancel one of the caller's commitments
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
              properties:
                action:
                  type: string
                  enum:
                    - renegotiate
                    - complete
                    - cancel
                dueAt:
                  type: string
                  format: date-time
                  description: Required for `renegotiate`.
      responses:
        "200":
          description: Commitment updated
        "400":
          description: Unknown action or missing dueAt
        "404":
          description: No open commitment with that id belongs to the caller
  /api/manager/checkins:
    get:
      tags:
        - Manager
      summary: The caller's own check-in history
      parameters:
        - name: unanswered
          in: query
          schema:
            type: string
            enum:
              - "true"
              - "false"
        - name: limit
          in: query
          schema:
            type: integer
            maximum: 100
      responses:
        "200":
          description: Check-ins
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  checkins:
                    type: array
                    items:
                      type: object
    post:
      tags:
        - Manager
      summary: Answer a check-in
      description: |
        Write-once — a check-in that already has an answer is not overwritten. Check-ins are created by the `manager-checkins` cron, never by this API: answering one earns points, so a user-creatable check-in would be a points faucet.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
                - response
              properties:
                id:
                  type: string
                  format: uuid
                response:
                  type: string
      responses:
        "200":
          description: Answer recorded
        "400":
          description: Invalid input
        "404":
          description: Check-in not found
          or already answered: null
        "409":
          description: Manager Mode is not enabled
  /api/manager/rewards:
    get:
      tags:
        - Manager
      summary: Points ledger, payout history, and what the caller could redeem now
      responses:
        "200":
          description: Reward status
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  status:
                    type: object
                  entries:
                    type: array
                    items:
                      type: object
                  payouts:
                    type: array
                    items:
                      type: object
    post:
      tags:
        - Manager
      summary: Redeem earned points for real account credits
      description: |
        The only Manager Mode endpoint that moves money. Requires both `rewardsEnabled` and a non-zero `monthlyCreditCap` on the caller's agreement; the payout is clamped by the monthly cap, then the point balance, then the amount requested. Points are debited in the same transaction as the payout row and refunded if the credit grant fails.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                credits:
                  type: integer
                  description: Omit to redeem as many as the caps and balance allow.
      responses:
        "200":
          description: Credits granted
        "409":
          description: Rewards disabled
          monthly cap reached: null
          or not enough points: null
        "502":
          description: The upstream credit grant failed; points were refunded
  /api/whiteboards:
    get:
      tags:
        - Canvas
      summary: List whiteboards in the workspace
      responses:
        "200":
          description: Whiteboards
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  whiteboards:
                    type: array
                    items:
                      type: object
                  workspaceId:
                    type: string
                    format: uuid
    post:
      tags:
        - Canvas
      summary: Create a whiteboard
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                projectRef:
                  type: string
                  nullable: true
                scene:
                  type: object
                  description: Serialized Excalidraw scene (capped at 5 MB).
                thumbnail:
                  type: string
                  nullable: true
      responses:
        "201":
          description: Whiteboard created
        "400":
          description: Invalid input
        "413":
          description: Scene exceeds the 5 MB limit
  /api/whiteboards/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Canvas
      summary: Get a whiteboard
      responses:
        "200":
          description: Whiteboard
        "404":
          description: Whiteboard not found
    patch:
      tags:
        - Canvas
      summary: Rename a whiteboard or save its scene
      description: Rename-only when `scene` is omitted and `title` is present; otherwise saves the scene with an optional optimistic-lock guard (`expectedUpdatedAt`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                scene:
                  type: object
                thumbnail:
                  type: string
                  nullable: true
                expectedUpdatedAt:
                  type: string
                  nullable: true
      responses:
        "200":
          description: Whiteboard updated
        "400":
          description: Invalid input
        "404":
          description: Whiteboard not found
        "409":
          description: Optimistic-lock conflict (stale copy)
        "413":
          description: Scene exceeds the 5 MB limit
    delete:
      tags:
        - Canvas
      summary: Delete a whiteboard
      responses:
        "200":
          description: Whiteboard deleted
        "404":
          description: Whiteboard not found
  /api/mindmaps:
    get:
      tags:
        - Canvas
      summary: List mind maps in the workspace
      responses:
        "200":
          description: Mind maps
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  mindmaps:
                    type: array
                    items:
                      type: object
    post:
      tags:
        - Canvas
      summary: Create a mind map
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                projectRef:
                  type: string
                  nullable: true
                graph:
                  type: object
                  description: "{ nodes, edges } graph (capped at 3 MB)."
      responses:
        "201":
          description: Mind map created
        "400":
          description: Invalid input
        "413":
          description: Graph payload too large
  /api/mindmaps/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Canvas
      summary: Get a mind map
      responses:
        "200":
          description: Mind map
        "404":
          description: Mind map not found
    patch:
      tags:
        - Canvas
      summary: Rename a mind map or save its graph
      description: Rename-only when `graph` is omitted and `title` is present; otherwise saves the graph with an optional optimistic-lock guard (`expectedUpdatedAt`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                graph:
                  type: object
                expectedUpdatedAt:
                  type: string
                  nullable: true
      responses:
        "200":
          description: Mind map updated
        "400":
          description: Invalid input
        "404":
          description: Mind map not found
        "409":
          description: Concurrent-edit conflict (stale copy)
        "413":
          description: Graph payload too large
    delete:
      tags:
        - Canvas
      summary: Delete a mind map
      responses:
        "200":
          description: Mind map deleted
        "404":
          description: Mind map not found
  /api/shares:
    get:
      tags:
        - Sharing
      summary: List public share links (admin)
      parameters:
        - name: resourceType
          in: query
          schema:
            type: string
            enum:
              - project
              - view
        - name: resourceId
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Share links
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  links:
                    type: array
                    items:
                      type: object
    post:
      tags:
        - Sharing
      summary: Mint a public read-only share link (admin)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - resourceType
                - resourceId
              properties:
                resourceType:
                  type: string
                  enum:
                    - project
                    - view
                resourceId:
                  type: string
                expiresAt:
                  type: string
                  format: date-time
                  nullable: true
      responses:
        "201":
          description: Link created (returns the one-time token + absolute URL)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  token:
                    type: string
                  url:
                    type: string
                  link:
                    type: object
        "400":
          description: Invalid input
    patch:
      tags:
        - Sharing
      summary: Revoke a public share link (admin)
      description: Alias of DELETE. Accepts `?id=` or a JSON body `{ id }`.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: string
      responses:
        "200":
          description: Link revoked
        "400":
          description: Share link id is required
        "404":
          description: Not found or already revoked
    delete:
      tags:
        - Sharing
      summary: Revoke a public share link (admin)
      parameters:
        - name: id
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Link revoked
        "400":
          description: Share link id is required
        "404":
          description: Not found or already revoked
  /api/p/share/{token}:
    get:
      tags:
        - Sharing
      summary: Resolve a public share token (no auth)
      description: Public, unauthenticated, read-only projection of a shared project/view. A single 404 covers every failure (missing/revoked/expired token, deleted resource). Rate-limited and marked noindex.
      security: []
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Sanitized share projection
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  share:
                    type: object
        "404":
          description: Share link is unavailable
        "429":
          description: Rate limit exceeded
  /api/proofing/{fileId}/annotations:
    parameters:
      - name: fileId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Proofing
      summary: List annotations on an attachment
      responses:
        "200":
          description: Annotations
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  annotations:
                    type: array
                    items:
                      type: object
    post:
      tags:
        - Proofing
      summary: Add an annotation pin to an attachment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - x
                - y
              properties:
                x:
                  type: number
                y:
                  type: number
                page:
                  type: number
                  default: 1
                comment:
                  type: string
                taskId:
                  type: integer
                  nullable: true
      responses:
        "201":
          description: Annotation created
        "400":
          description: Invalid input
        "404":
          description: Attachment not found
  /api/proofing/{fileId}/annotations/{annotationId}:
    parameters:
      - name: fileId
        in: path
        required: true
        schema:
          type: string
      - name: annotationId
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Proofing
      summary: Edit an annotation comment and/or toggle resolved
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                comment:
                  type: string
                resolved:
                  type: boolean
      responses:
        "200":
          description: Annotation updated
        "400":
          description: Invalid input
        "404":
          description: Annotation not found
    delete:
      tags:
        - Proofing
      summary: Delete an annotation
      responses:
        "200":
          description: Annotation deleted
        "404":
          description: Annotation not found
  /api/projects/folders:
    get:
      tags:
        - Projects
      summary: List project folders
      responses:
        "200":
          description: Folders
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  folders:
                    type: array
                    items:
                      type: object
                  canManage:
                    type: boolean
    post:
      tags:
        - Projects
      summary: Create a project folder (admin)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                color:
                  type: string
                  nullable: true
      responses:
        "200":
          description: Folder created
        "400":
          description: Invalid input
    patch:
      tags:
        - Projects
      summary: Reorder folders or update one folder (admin)
      description: Reorder mode when `order` (string[] of ids) is supplied; otherwise updates the folder identified by `id`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                order:
                  type: array
                  items:
                    type: string
                id:
                  type: string
                name:
                  type: string
                color:
                  type: string
                  nullable: true
      responses:
        "200":
          description: Folders updated
        "400":
          description: Invalid input
        "404":
          description: Folder not found
    delete:
      tags:
        - Projects
      summary: Delete a project folder (admin)
      description: Accepts `?id=` or a JSON body `{ id }`. Projects in the folder fall back to Uncategorized.
      parameters:
        - name: id
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Folder deleted
        "400":
          description: Folder id is required
        "404":
          description: Folder not found
  /api/geocode:
    get:
      tags:
        - Tasks
      summary: Forward-geocode an address (task Map view)
      description: Server-side proxy to OpenStreetMap Nominatim. A null result means the address could not be located (not an error).
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
            maxLength: 300
      responses:
        "200":
          description: Geocode result (or null)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  cached:
                    type: boolean
                  result:
                    nullable: true
                    type: object
                    properties:
                      lat:
                        type: number
                      lng:
                        type: number
                      displayName:
                        type: string
        "400":
          description: Missing or too-long address
        "502":
          description: Geocoding service unavailable
  /api/integrations/trello/import/preview:
    post:
      tags:
        - Integrations
      summary: Dry-run a Trello board export (admin)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                content:
                  type: string
                  description: Trello board JSON export.
      responses:
        "200":
          description: Import preview (columns / counts / assignee matches)
        "400":
          description: Could not parse / nothing importable
  /api/integrations/trello/import/run:
    post:
      tags:
        - Integrations
      summary: Import a Trello board export as a new project (admin)
      description: Idempotent — re-importing the same board skips instead of duplicating.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                content:
                  type: string
                projectName:
                  type: string
      responses:
        "200":
          description: Import committed (or skipped if already imported)
        "400":
          description: Could not parse / import failed
  /api/integrations/monday/import/preview:
    post:
      tags:
        - Integrations
      summary: Dry-run a Monday.com export (admin)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                content:
                  type: string
                format:
                  type: string
                  enum:
                    - csv
                    - json
      responses:
        "200":
          description: Import preview (columns / counts / assignee matches)
        "400":
          description: Could not parse / nothing importable
  /api/integrations/monday/import/run:
    post:
      tags:
        - Integrations
      summary: Import a Monday.com export as a new project (admin)
      description: Idempotent — re-importing the same export skips instead of duplicating.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                content:
                  type: string
                format:
                  type: string
                  enum:
                    - csv
                    - json
                projectName:
                  type: string
      responses:
        "200":
          description: Import committed (or skipped if already imported)
        "400":
          description: Could not parse / import failed
  /api/integrations/notion/connect-token:
    post:
      tags:
        - Integrations
      summary: Connect Notion with an internal integration token
      description: Fallback for Notion's OAuth flow — paste an internal integration token (`secret_…` / `ntn_…`). The token is probed against /v1/users/me and persisted. POST and PUT behave identically.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
              properties:
                token:
                  type: string
      responses:
        "200":
          description: Connected (returns the Notion bot user)
        "400":
          description: Missing / malformed token
        "404":
          description: Unknown provider
        "502":
          description: Connection test failed
    put:
      tags:
        - Integrations
      summary: Connect Notion with an internal integration token
      description: Identical to POST.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
              properties:
                token:
                  type: string
      responses:
        "200":
          description: Connected (returns the Notion bot user)
        "400":
          description: Missing / malformed token
        "404":
          description: Unknown provider
        "502":
          description: Connection test failed
  /api/integrations/notion/import:
    get:
      tags:
        - Integrations
      summary: List the Notion databases the connection can see
      responses:
        "200":
          description: Databases (for the import picker)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  databases:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        title:
                          type: string
        "412":
          description: Notion is not connected
        "502":
          description: Failed to list databases
    post:
      tags:
        - Integrations
      summary: Import a Notion database into a project as tasks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - databaseId
                - projectRef
              properties:
                databaseId:
                  type: string
                projectRef:
                  type: string
                limit:
                  type: integer
      responses:
        "200":
          description: Import report
        "400":
          description: Invalid input / import error
        "404":
          description: Unknown provider
        "412":
          description: Notion is not connected
        "502":
          description: Import failed
  /api/people/profile:
    get:
      tags:
        - People
      summary: List member experience profiles, or fetch one in full
      description: |
        Without `member`, returns every member's profile with `resumeText` elided. With `member`, returns that member's full profile including the resume text. Profiles are keyed by member display name (matching `app_tasks.assigned_to`), not user id, so roster-only members are covered.
      parameters:
        - name: member
          in: query
          schema:
            type: string
          description: Return one member's full profile by display name.
      responses:
        "200":
          description: Profile
          or the elided profile list: null
        "400":
          description: Workspace required
        "401":
          description: Unauthorized
        "403":
          description: Requires the invites/view permission
    put:
      tags:
        - People
      summary: Partial-update a member's experience profile
      description: |
        Omitted fields keep their stored value; an empty string clears a field. When the resume or experience text changes and `deriveSkills` is not false, the profile text is mapped onto the canonical categorized skill vocabulary and merged additively into the member's declared skills (an LLM pass never wipes hand-declared skills).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - memberName
              properties:
                memberName:
                  type: string
                  minLength: 1
                  maxLength: 200
                linkedinUrl:
                  type: string
                  description: Must be a valid http(s) URL.
                experienceSummary:
                  type: string
                  description: Free-text background blurb.
                resumeText:
                  type: string
                  description: Resume as TEXT (extracted client-side from PDF/txt/md), not a file.
                resumeFileName:
                  type: string
                deriveSkills:
                  type: boolean
                  default: true
                  description: Derive declared skills from the profile text after saving.
      responses:
        "200":
          description: Updated profile plus any derivedSkills
        "400":
          description: Invalid body or LinkedIn URL
        "401":
          description: Unauthorized
        "403":
          description: Requires the invites/create permission
  /api/people/team-context:
    get:
      tags:
        - People
      summary: Get the workspace team context brief used for AI assignment
      description: |
        A standing free-text block (who is strong at what, hiring gaps, assignment conventions) injected into the AI agent's system prompt. Any member may read it.
      responses:
        "200":
          description: Team context
        "400":
          description: Workspace required
        "401":
          description: Unauthorized
    put:
      tags:
        - People
      summary: Replace the workspace team context brief (admin)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - context
              properties:
                context:
                  type: string
      responses:
        "200":
          description: Team context saved
        "400":
          description: Invalid body
        "401":
          description: Unauthorized
        "403":
          description: Admin only
  /api/profile/experience:
    get:
      tags:
        - Personal
      summary: Get your own experience profile and declared skills
      description: |
        Self-service sibling of `/api/people/profile` — no people-management permission required. The member name is resolved SERVER-SIDE from the session, never from the request, so a caller can only reach their own row.
      responses:
        "200":
          description: Your profile and skills
        "400":
          description: Workspace required
        "401":
          description: Unauthorized
    put:
      tags:
        - Personal
      summary: Update your own experience profile (and optionally declared skills)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                linkedinUrl:
                  type: string
                experienceSummary:
                  type: string
                resumeText:
                  type: string
                resumeFileName:
                  type: string
                skills:
                  type: array
                  description: Replaces your declared skill set (same shape as /api/people/skills).
                  items:
                    type: object
                    required:
                      - skill
                      - strength
                    properties:
                      skill:
                        type: string
                        description: A canonical vocabulary key, or your own custom free-text skill.
                      strength:
                        type: number
                        minimum: 0
                        maximum: 1
                deriveSkills:
                  type: boolean
                  default: true
      responses:
        "200":
          description: Updated profile plus any derivedSkills
        "400":
          description: Invalid body
        "401":
          description: Unauthorized
  /api/ai-agent/activity:
    get:
      tags:
        - AI Agent
      summary: Agent activity timeline — individual ledger rows (admin)
      description: |
        One row per agent write, newest first. This is the row-level companion to /api/ai-agent/activity-report, which returns only aggregates ("47 actions, 3 approvals") and so answers how much the agent did but never what.
        Admin-only: the ledger spans every member's agent use.
        Opt-in — returns 403 `agent_oversight_disabled` unless the workspace has enabled the activity ledger viewer. The ledger keeps RECORDING either way; the flag only governs whether the workspace has asked to see it.
        Paginates by keyset cursor on `created_at` (`before`), which is stable under concurrent writes where an offset would not be. `next_cursor` is null on the last page. Unknown filter values are ignored rather than rejected, so a stale bookmark shows the unfiltered timeline.
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
        - name: before
          in: query
          description: Keyset cursor — return rows older than this ISO timestamp.
          schema:
            type: string
            format: date-time
        - name: status
          in: query
          schema:
            type: string
            enum:
              - executed
              - parked
              - blocked
              - failed
              - undone
        - name: run_kind
          in: query
          schema:
            type: string
            enum:
              - interactive
              - autonomous
              - scheduled
        - name: category
          in: query
          description: Action-policy category (external_send, finance, destructive, …).
          schema:
            type: string
        - name: user_id
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Actions plus `next_cursor` (null on the last page)
        "400":
          description: Workspace required
        "401":
          description: Unauthorized
        "403":
          description: Admin only, or the activity ledger is disabled (`agent_oversight_disabled`)
  /api/ai-agent/approvals:
    get:
      tags:
        - AI Agent
      summary: Global agent approvals inbox (admin)
      description: |
        Every action the agent has parked for a human, workspace-wide, plus the pending count.
        This is the only route that can surface an approval from an event-driven run (a new email, a PR), which has no task to navigate to. Deliberately NOT gated on the oversight opt-in for that reason — gating it would strand those approvals in workspaces that had not opted in.
        Admin-only. The rows enumerate every outbound email and payment the agent is holding; `tool_input` is stripped from the payload, but the inventory itself is privileged and only admins can resolve an approval anyway.
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        "200":
          description: Parked approvals plus the pending count
        "400":
          description: Workspace required
        "401":
          description: Unauthorized
        "403":
          description: Admin only
  /api/ai-agent/approvals/{approvalId}:
    get:
      tags:
        - AI Agent
      summary: One parked action, in full - the reply desk (admin)
      description: |
        The inbox list deliberately omits `tool_input`; this is where an admin reads it, because approving means executing it and nobody should approve what they cannot see.
        For outbound mail this is the REPLY DESK view: the draft, the inbound message it answers, the rest of that thread, the correspondent's Deal Room history, and the mailbox the reply would send from (the RUN's user, not the approver's). Everything is read from what Kavanah already mirrored - no provider call, so reviewing a reply never depends on, or spends, the mailbox owner's live credential.
        `approval.draft_digest` fingerprints the parked arguments as they stand right now. Echo it back as `reviewed_digest` when approving; an edited approval REQUIRES it, so an edit can never land on a draft that changed after it was read.
        Admin-only, like resolving one.
      parameters:
        - name: approvalId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The approval (with `tool_input` and `draft_digest`) and, for outbound mail, `reply_desk`
        "400":
          description: Workspace required
        "401":
          description: Unauthorized
        "403":
          description: Admin only
        "404":
          description: No approval with that id in this workspace
    post:
      tags:
        - AI Agent
      summary: Approve or deny a parked agent action (admin)
      description: |
        Resolve a parked action from the global inbox — the task-independent sibling of /api/tasks/{id}/agent-run/approvals/{approvalId}, and the only way to resolve an approval from an event-driven run.
        Approving means EXECUTING the action (sending the email, moving the money), and the actions that park are exactly the dangerous ones — so this requires an admin, which is stricter than the task-scoped route's `tasks:edit`.
        Shares the approvals service with the task-scoped route, so both re-check expiry, guardrails, and policy at resolve time rather than trusting the decision made when the action was parked.
      parameters:
        - name: approvalId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - decision
              properties:
                decision:
                  type: string
                  enum:
                    - approve
                    - deny
                edits:
                  type: object
                  description: |
                    Reply desk only, and only with `decision: approve`: a principal's rewrite of the draft. Only the TEXT may be changed (`subject`/`body`, plus `html` for an AngelSend draft); naming any other field - a recipient above all - is REFUSED with 400 rather than ignored. The edited text is what is sent, `tool_input` keeps the agent's draft, and the ledger row carries both.
                  properties:
                    subject:
                      type: string
                    body:
                      type: string
                    html:
                      type: string
                reviewed_digest:
                  type: string
                  description: |
                    `approval.draft_digest` from GET on this path. Required alongside `edits`; a mismatch is 409, because an edit is consent to one specific message.
      responses:
        "200":
          description: Resolved — the outcome of the execution (or the denial)
        "400":
          description: Workspace required, an invalid decision, or an edit this draft does not allow
        "401":
          description: Unauthorized
        "403":
          description: Admin only
        "404":
          description: No actionable approval with that id
        "409":
          description: Blocked on re-check (a guardrail or policy refused it at resolve time), already resolved, or the draft changed after it was reviewed
        "410":
          description: The approval expired
        "502":
          description: The approved action failed to execute
  /api/ai-agent/personas/org-chart:
    get:
      tags:
        - AI Agent
      summary: The AI-employee org chart
      description: |
        Every AI Employee the caller may see, with its reporting line (`reports_to_persona_id`) and departments, for the org-chart view.
        Same visibility rule as GET /api/ai-agent/personas (one helper, two callers): a member never sees a restricted or disabled employee, and a visible employee whose MANAGER is hidden from them has its `reports_to_persona_id` withheld - so the chart cannot be used to learn that a restricted employee exists.
      responses:
        "200":
          description: The visible employees with their reporting lines and departments
        "400":
          description: Workspace required
        "401":
          description: Unauthorized
        "403":
          description: The AI agent is disabled for this workspace
  /api/deals/{id}/team:
    get:
      tags:
        - Deals
      summary: The deal team (our side)
      description: |
        Who on OUR side is on the deal: the lead first (role `lead` — this is the deal's owner, `owner_user_id`, rendered with their profile), then the roster members (role `member`) with each one's title on this deal, in the order they were added. `isActiveMember` is false for someone who has since left the workspace; the roster keeps them for the record.
        Needs view access on the deal, through the same record gate as `GET /api/deals/{id}` — a private deal's roster says who is working on it. The same list is included as `team` in the deal detail payload.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: "`team`: the lead then the members"
        "400":
          description: No workspace
        "401":
          description: Unauthorized
        "403":
          description: No view access on this deal
        "404":
          description: Deal not found
    put:
      tags:
        - Deals
      summary: Replace the deal team
      description: |
        A whole-roster write. `members` is the ENTIRE member list — anyone absent from it is removed, titles are updated to what is sent. `lead` is optional: absent leaves the lead alone, `null` clears it, a user id makes that person the lead. The lead IS `app_deals.owner_user_id`, so a lead change is the same write as `PATCH /api/deals/{id}` with `ownerUserId` — it records the `owner_change` activity and resolves `owner_name` from the profile. When the lead changes, the previous lead is kept on the deal only if `members` names them.
        Every member must be an ACTIVE member of this workspace. An id that is not, or a title over 80 characters, is returned in `rejected` with a reason, and the rest of the list is still applied — nothing is dropped silently. Needs edit access on the deal.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - members
              properties:
                lead:
                  type:
                    - string
                    - "null"
                  format: uuid
                  description: New lead's user id, or null to clear. Omit to leave it.
                members:
                  type: array
                  maxItems: 100
                  items:
                    type: object
                    required:
                      - userId
                    properties:
                      userId:
                        type: string
                        format: uuid
                      title:
                        type:
                          - string
                          - "null"
                        maxLength: 80
                        description: What they do on this deal ("Diligence", "Legal").
      responses:
        "200":
          description: "`team` as it now stands, and `rejected` ({ userId, reason }[])"
        "400":
          description: Malformed body
        "401":
          description: Unauthorized
        "403":
          description: No edit access on this deal
        "404":
          description: Deal not found
  /api/deals/{id}/data-room-analysis:
    get:
      tags:
        - Deals
      summary: The deal's stored data-room reading
      description: |
        The last reading of the deal's Google Drive data room: status, which files were read or skipped and why, the sourced insight library (each insight with its file name and a passage quoted verbatim from it), how many proposed insights were dropped as unsupported, and whether automatic analysis (`data_room_analysis_enabled`) is on for the workspace. Free — reads nothing from Drive. `analysis` is null when the room was never read. Needs view access on the deal.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: "`analysis` (or null) and `automaticEnabled`"
        "400":
          description: No workspace
        "401":
          description: Unauthorized
        "403":
          description: No view access on this deal
        "404":
          description: Deal not found
    post:
      tags:
        - Deals
      summary: Read the deal's data room now
      description: |
        Reads up to 12 documents from the deal's Drive folder and records the investment profile plus 6-10 sourced insights. Every insight's quote is checked against the extracted text and every number in it must appear in its quote; anything else is dropped. One AI call billed to the signed-in user (402 when they cannot cover it); not gated on the automatic-analysis flag. A profile someone recorded by hand is kept unless `overwriteProfile` is true. With `force` false, an unchanged folder returns the stored reading without a model call. Needs edit access on the deal. Rate-limited to 3 per deal per minute.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                force:
                  type: boolean
                  description: Re-read even if the folder has not changed.
                overwriteProfile:
                  type: boolean
                  description: Replace a hand-recorded investment profile.
      responses:
        "200":
          description: "`outcome` (analyzed | unchanged), `profileWritten`, `profileNote`, `summary`, `analysis`"
        "400":
          description: Malformed body
        "401":
          description: Unauthorized
        "402":
          description: Not enough credits for the reading
        "403":
          description: No edit access on this deal
        "404":
          description: Deal not found
        "422":
          description: No folder, no Drive connection, an empty folder, or nothing readable (`code` says which)
        "429":
          description: Rate limited
        "501":
          description: AI not configured
        "502":
          description: Drive or the model failed
  /api/deals/conversations:
    get:
      tags:
        - Deals
      summary: The Rain Room conversation map
      description: |
        One row per (deal, contact) conversation, plus one row per contact on NO open deal who has contact-level mail or an active network cadence enrollment (`dealId: null`, `source: "network"`): when they last wrote, when we last wrote, whose turn it is, days since, the active cadence step and the desk's next step. Sorted "we owe a reply" first, OLDEST first.
        Whose turn it is comes from EMAIL only - a meeting or a call is two-way and settles nothing about who owes the next message. A deal with no email of its own falls back to the contact's last mail anywhere, and the row says so (`source: "contact"`, timestamps only). A no-deal row's mail is the contact's own, so `source: "network"` carries its summaries.
        `stage`, `pipeline` and a user-id `owner` are deal filters: when any is set, no-deal rows are left out. `owner=unassigned` includes them (a contact on no deal has no owner).
        Private deals the caller cannot open are excluded, exactly as in the deal list. Bounded: past the cap the response says `truncated: true` rather than returning a silent sample.
      parameters:
        - name: owner
          in: query
          description: A user id, `me`, or `unassigned`.
          schema:
            type: string
        - name: stage
          in: query
          schema:
            type: string
            format: uuid
        - name: pipeline
          in: query
          schema:
            type: string
            format: uuid
        - name: include_closed
          in: query
          description: "`1` to include won/lost deals. Off by default - the desk works live deals."
          schema:
            type: string
            enum:
              - "1"
      responses:
        "200":
          description: "`conversations`, `counts` by turn, and `truncated`"
        "400":
          description: No workspace, or a malformed owner/stage/pipeline id
        "401":
          description: Unauthorized
  /api/ai-agent/outcomes:
    get:
      tags:
        - AI Agent
      summary: Agent outcome checks and hit rate (admin)
      description: |
        "Did the agent's work actually land?" The activity ledger answers what the agent DID; this answers whether it WORKED. Each row is a claim the agent made about the world, settled against the database by the verify-outcomes cron — met, missed, or cancelled when the referenced thing vanished and the claim became undecidable.
        The hit rate counts only SETTLED checks: pending ones are not evidence yet, and cancelled ones are not evidence either way.
        Opt-in — returns 403 `agent_oversight_disabled` unless outcome verification is enabled for the workspace.
      parameters:
        - name: days
          in: query
          description: Scorecard window. Default 30.
          schema:
            type: integer
            default: 30
            minimum: 1
        - name: status
          in: query
          schema:
            type: string
            enum:
              - pending
              - met
              - missed
              - cancelled
      responses:
        "200":
          description: Expectations, the scorecard, and `escalate_on_failure`
        "401":
          description: Unauthorized
        "403":
          description: Admin only, or outcome verification is disabled (`agent_oversight_disabled`)
    post:
      tags:
        - AI Agent
      summary: Cancel a pending outcome check (admin)
      description: |
        Exclude a pending check a human judges unfair — the task was descoped, the person left — rather than letting it count as a miss.
        Audited (`ai_agent.outcome_check_cancelled`), because "cancel the check that was about to fail" is precisely how this ledger could be laundered into looking clean.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - expectation_id
              properties:
                expectation_id:
                  type: string
                  minLength: 1
                  maxLength: 64
      responses:
        "200":
          description: Cancelled
        "400":
          description: Invalid body
        "401":
          description: Unauthorized
        "403":
          description: Admin only, or outcome verification is disabled (`agent_oversight_disabled`)
        "404":
          description: No pending outcome check with that id
  /api/ai-agent/action-policies:
    get:
      tags:
        - AI Agent
      summary: List agent action policies and the category catalog (admin)
      description: |
        Per-category policy governing risky agent actions. Categories are external_send, finance, destructive, membership, deploy, external_write. No row for a category means "park for approval" (fail closed).
      responses:
        "200":
          description: Policies plus the category catalog
        "401":
          description: Unauthorized
        "403":
          description: Admin only
    put:
      tags:
        - AI Agent
      summary: Bulk upsert agent action policies (admin)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - policies
              properties:
                policies:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required:
                      - category
                      - mode
                    properties:
                      persona_id:
                        type: string
                        nullable: true
                        description: Persona-scoped policy. A persona row shadows the workspace row.
                      category:
                        type: string
                        enum:
                          - external_send
                          - finance
                          - destructive
                          - membership
                          - deploy
                          - external_write
                      mode:
                        type: string
                        enum:
                          - allow
                          - allow_budget
                          - approve
                          - block
                      daily_limit:
                        type: integer
                        minimum: 1
                        maximum: 1000
                        description: Required when mode is allow_budget — executed actions per UTC day.
      responses:
        "200":
          description: Saved policies
        "400":
          description: Unknown category/mode
          or a missing daily_limit: null
        "401":
          description: Unauthorized
        "403":
          description: Admin only
    delete:
      tags:
        - AI Agent
      summary: Delete an agent action policy (admin)
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Deleted
        "400":
          description: Missing id
        "401":
          description: Unauthorized
        "403":
          description: Admin only
        "404":
          description: Policy not found
  /api/ai-agent/escalation-policy:
    get:
      tags:
        - AI Agent
      summary: Get the workspace agent escalation policy (admin)
      responses:
        "200":
          description: Escalation policy
        "401":
          description: Unauthorized
        "403":
          description: Admin only
    put:
      tags:
        - AI Agent
      summary: Update the workspace agent escalation policy (admin)
      description: |
        Partial update — omitted fields keep their current value. Recipients are validated against active workspace membership; `notify_user_ids: null` means "all admins".
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                min_immediate_severity:
                  type: string
                  enum:
                    - info
                    - warning
                    - critical
                quiet_hours_enabled:
                  type: boolean
                quiet_start_hour:
                  type: integer
                  minimum: 0
                  maximum: 23
                quiet_end_hour:
                  type: integer
                  minimum: 0
                  maximum: 23
                timezone:
                  type: string
                  description: IANA timezone, e.g. America/Los_Angeles.
                critical_overrides_quiet_hours:
                  type: boolean
                notify_user_ids:
                  type: array
                  nullable: true
                  items:
                    type: string
                    format: uuid
      responses:
        "200":
          description: Saved policy
        "400":
          description: No provided recipient is an active member
        "401":
          description: Unauthorized
        "403":
          description: Admin only
  /api/ai-agent/slack-managers:
    get:
      tags:
        - AI Agent
      summary: List AI Slack managers, with their last 7 days of answers (admin)
      description: |
        An AI Slack manager is an AI employee that answers people in a connected Slack workspace — @mentions of the Kavanah bot in chosen channels, and optionally DMs. It has no tools and sees no workspace data; it knows its persona and its playbook only. Each manager carries `stats` for the last 7 days (answered, escalated, rate-limited, refused, failed, credits, distinct people).
      responses:
        "200":
          description: The workspace's Slack managers
        "401":
          description: Unauthorized
        "403":
          description: Admin only
    post:
      tags:
        - AI Agent
      summary: Create an AI Slack manager (admin)
      description: |
        The caller becomes the manager's OWNER: their Slack connection posts the answers and their credits pay for them, so the Slack workspace must be one the caller has connected. Created switched off unless `enabled: true` is sent. snake_case keys are accepted too.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SlackManagerInput"
      responses:
        "201":
          description: Created manager
        "400":
          description: Invalid field
          a digital twin: null
          a Slack workspace the caller has not connected: null
          or switched on with nowhere to answer: null
        "401":
          description: Unauthorized
        "403":
          description: Admin only
        "409":
          description: Another enabled manager already answers that channel
          every channel: null
          or DMs: null
  /api/ai-agent/slack-managers/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - AI Agent
      summary: Update an AI Slack manager (admin)
      description: |
        Omitted fields are left unchanged. The owner never changes; every Slack check is made against the owner's connection.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SlackManagerInput"
      responses:
        "200":
          description: Updated manager
        "400":
          description: Invalid field
        "401":
          description: Unauthorized
        "403":
          description: Admin only
        "404":
          description: Not found in this workspace
        "409":
          description: Conflicts with another enabled manager
    delete:
      tags:
        - AI Agent
      summary: Delete an AI Slack manager and its reply log (admin)
      responses:
        "200":
          description: Deleted
        "401":
          description: Unauthorized
        "403":
          description: Admin only
        "404":
          description: Not found in this workspace
  /api/ai-agent/guardrails:
    get:
      tags:
        - AI Agent
      summary: List agent guardrail rules and the catalog (admin)
      parameters:
        - name: persona_id
          in: query
          schema:
            type: string
          description: Filter to one persona's rules.
      responses:
        "200":
          description: Rules plus the guardrail catalog
        "401":
          description: Unauthorized
        "403":
          description: Admin only
    post:
      tags:
        - AI Agent
      summary: Create an agent guardrail rule (admin)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - pattern
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 120
                pattern:
                  type: string
                  minLength: 1
                  maxLength: 300
                rule_type:
                  type: string
                  enum:
                    - recipient
                    - keyword
                  default: recipient
                match_mode:
                  type: string
                  enum:
                    - exact
                    - domain
                    - contains
                  default: exact
                action:
                  type: string
                  enum:
                    - block
                    - approve
                  default: block
                reason:
                  type: string
                  maxLength: 300
                persona_id:
                  type: string
                  description: Scope the rule to one persona.
      responses:
        "200":
          description: Created rule
        "400":
          description: Invalid body or unknown persona
        "401":
          description: Unauthorized
        "403":
          description: Admin only
    patch:
      tags:
        - AI Agent
      summary: Update an agent guardrail rule (admin)
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 120
                pattern:
                  type: string
                  minLength: 1
                  maxLength: 300
                rule_type:
                  type: string
                  enum:
                    - recipient
                    - keyword
                match_mode:
                  type: string
                  enum:
                    - exact
                    - domain
                    - contains
                action:
                  type: string
                  enum:
                    - block
                    - approve
                enabled:
                  type: boolean
      responses:
        "200":
          description: Updated rule
        "400":
          description: Invalid field
        "401":
          description: Unauthorized
        "403":
          description: Admin only
        "404":
          description: Rule not found
    delete:
      tags:
        - AI Agent
      summary: Delete an agent guardrail rule (admin)
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Deleted
        "400":
          description: Missing id
        "401":
          description: Unauthorized
        "403":
          description: Admin only
        "404":
          description: Rule not found
  /api/ai-agent/activity-report:
    get:
      tags:
        - AI Agent
      summary: Agent activity/self-report — totals, categories, top tools, approvals (admin)
      parameters:
        - name: days
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 90
            default: 7
          description: Lookback window in days (clamped to 90).
      responses:
        "200":
          description: Activity report
        "401":
          description: Unauthorized
        "403":
          description: Admin only
  /api/ai-agent/schedules/runs:
    get:
      tags:
        - AI Agent
      summary: List scheduled autonomous agent runs
      parameters:
        - name: schedule
          in: query
          schema:
            type: string
          description: Filter to one schedule id.
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
      responses:
        "200":
          description: 'Runs (each with the `model` it ran on and `auto_model_tier`, the ladder rung when a tier said "auto"; both null on older rows and skips), the schedules they belong to (each with its `model`: null = inherited, "auto", or a model id), whether the caller can manage them, and `model_options` for the schedule model picker'
        "401":
          description: Unauthorized
  /api/ai-agent/share:
    post:
      tags:
        - AI Agent
      summary: Share an agent answer or conversation
      description: |
        Shares a single message or a whole conversation. `destination=knowledge` saves it as a knowledge document; `destination=snapshot` creates a snapshot that can additionally be DM'd to recipients or made public via a link.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                scope:
                  type: string
                  enum:
                    - message
                    - conversation
                  default: message
                destination:
                  type: string
                  enum:
                    - snapshot
                    - knowledge
                  default: snapshot
                visibility:
                  type: string
                  enum:
                    - workspace
                    - public
                  default: workspace
                title:
                  type: string
                note:
                  type: string
                content:
                  type: string
                  description: Required for a message-scope snapshot.
                source_conversation_id:
                  type: string
                recipient_user_ids:
                  type: array
                  maxItems: 20
                  items:
                    type: string
                    format: uuid
                  description: DM the snapshot to these members (active members of this workspace only).
      responses:
        "200":
          description: Snapshot (with share url when public) or the created knowledge document
        "400":
          description: Invalid body / missing content / invalid recipients
        "401":
          description: Unauthorized
        "404":
          description: Conversation not found
        "501":
          description: No LLM configured
  /api/ai-agent/share/{id}:
    get:
      tags:
        - AI Agent
      summary: Read a shared agent snapshot
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Snapshot
        "401":
          description: Unauthorized
        "404":
          description: Not found
  /api/integrations/gmail/inbox/status:
    get:
      tags:
        - Integrations
      summary: Gmail inbox connection status
      description: |
        Resolves whichever Gmail backend the user has connected — the AngelSend bridge first, then native Google OAuth. `has_gmail_read: false` on a connected account means the token predates the gmail.readonly scope and the user must reconnect (not connect).
      responses:
        "200":
          description: Connection status, backend, and read-scope flag
        "401":
          description: Unauthorized
  /api/integrations/gmail/inbox/messages:
    get:
      tags:
        - Integrations
      summary: List Gmail messages, or fetch one with its body
      parameters:
        - name: id
          in: query
          schema:
            type: string
          description: Fetch a single message including its body.
        - name: q
          in: query
          schema:
            type: string
          description: Gmail search query.
        - name: maxResults
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: pageToken
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Message envelopes
          or a single message: null
        "401":
          description: Unauthorized
        "412":
          description: No Gmail backend connected
  /api/integrations/gmail/inbox/send:
    post:
      tags:
        - Integrations
      summary: Send an email through the connected Gmail backend
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - to
                - subject
                - body
              properties:
                to:
                  oneOf:
                    - type: string
                    - type: array
                      items:
                        type: string
                subject:
                  type: string
                body:
                  type: string
                from:
                  type: string
                cc:
                  type: array
                  items:
                    type: string
                bcc:
                  type: array
                  items:
                    type: string
                is_html:
                  type: boolean
                thread_id:
                  type: string
                  description: Reply into an existing thread.
      responses:
        "200":
          description: Sent
        "400":
          description: Missing to/subject/body
        "401":
          description: Unauthorized
        "412":
          description: No Gmail backend connected
  /api/integrations/icloud/messages:
    get:
      tags:
        - Integrations
      summary: List synced iCloud Mail envelopes
      description: Envelope-only (IMAP). Bodies are not synced.
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: Message envelopes
        "401":
          description: Unauthorized
        "412":
          description: iCloud is not connected
    post:
      tags:
        - Integrations
      summary: Send an email via iCloud SMTP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - to
                - subject
                - body
              properties:
                to:
                  type: string
                subject:
                  type: string
                body:
                  type: string
                cc:
                  type: string
                bcc:
                  type: string
                skipDefaultBcc:
                  type: boolean
      responses:
        "200":
          description: Sent
        "400":
          description: Missing to/subject/body
        "401":
          description: Unauthorized
        "412":
          description: iCloud is not connected
  /api/integrations/icloud/events:
    get:
      tags:
        - Integrations
      summary: List iCloud calendar events (read live over CalDAV)
      parameters:
        - name: timeMin
          in: query
          schema:
            type: string
            format: date-time
          description: "Alias: from."
        - name: timeMax
          in: query
          schema:
            type: string
            format: date-time
          description: "Alias: to."
      responses:
        "200":
          description: Events
        "401":
          description: Unauthorized
        "412":
          description: iCloud is not connected
        "502":
          description: CalDAV read failed
    post:
      tags:
        - Integrations
      summary: Create an iCloud calendar event
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - summary
                - start
                - end
              properties:
                summary:
                  type: string
                start:
                  type: string
                  format: date-time
                end:
                  type: string
                  format: date-time
                location:
                  type: string
                description:
                  type: string
      responses:
        "200":
          description: Created event
        "400":
          description: Missing summary/start/end
        "401":
          description: Unauthorized
        "412":
          description: iCloud is not connected
  /api/browser/sessions:
    get:
      tags:
        - Integrations
      summary: List browser sessions visible to the caller
      description: The caller's own sessions plus any shared with the workspace. `mine` distinguishes them; `profileBacked` marks a session started from a saved login.
      responses:
        "200":
          description: Sessions and the caller's live count
        "401":
          description: Unauthorized
    post:
      tags:
        - Integrations
      summary: Start a cloud browser session
      description: Spends the user's Scrappy credits up front and meters per minute until released. Passing `profileId` reuses a saved login, so the browser starts already signed in.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                profileId:
                  type: string
                  description: Saved login to start from. Must belong to the caller.
                title:
                  type: string
                screenWidth:
                  type: integer
                screenHeight:
                  type: integer
                rotateEgress:
                  type: boolean
                  description: "Start on a fresh network exit, keeping the profile's cookies and user agent. The recovery from a 503 on a profile-backed session: the exit is what failed, so dropping the profile to escape it would discard the saved login too."
      responses:
        "200":
          description: Session started. `pinned` (present only when profileId was sent) reports whether the session actually egressed through the profile's pinned exit; false means the saved login is not IP-stable this run.
        "401":
          description: Unauthorized
        "402":
          description: The user's Scrappy credits are exhausted. The connection itself is healthy.
        "404":
          description: Saved login not found
        "412":
          description: Scrappy is not connected
        "429":
          description: Concurrent-session cap reached
        "502":
          description: Scrappy rejected the request
        "503":
          description: Scrappy could not reach the browser's network exit. Transient and upstream's own fault — retryable, and NOT a signal about the user's credential.
  /api/browser/sessions/{id}:
    delete:
      tags:
        - Integrations
      summary: Release a browser session
      description: Owner-only. Frees capacity and stops the per-minute meter. The local record is marked ended even if the upstream release fails, so a dead session never lingers in the list looking usable.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Released
        "401":
          description: Unauthorized
        "404":
          description: Session not found or not owned by the caller
        "412":
          description: Scrappy is not connected
  /api/browser/sessions/{id}/viewer-token:
    post:
      tags:
        - Integrations
      summary: Mint a short-lived viewer URL for the embedded browser
      description: Returns an absolute viewer URL with a ~5-minute signed token embedded, bound to (session, user). The Scrappy API key is never returned and never reaches the client. Available to anyone who may drive the session.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: viewerUrl and its expiry
        "401":
          description: Unauthorized
        "404":
          description: Session not found
        "409":
          description: Session has ended
        "412":
          description: Scrappy is not connected
        "502":
          description: Scrappy rejected the request
        "503":
          description: "Either the session is briefly unreachable while Scrappy rebuilds it under the same id (`kind: session_rebuilding` — retry, do not start a new session), or Scrappy could not reach its network exit (`kind: upstream_unavailable`). Both are retryable."
  /api/browser/sessions/{id}/keepalive:
    post:
      tags:
        - Integrations
      summary: Refresh a session's idle timer without acting on the page
      description: Reaping is idle-based, so a human reading a page or completing MFA looks identical to an abandoned session. The viewer calls this while mounted and visible.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Idle timer refreshed
        "401":
          description: Unauthorized
        "404":
          description: Session not found
        "409":
          description: Session was already reaped; the local record has been reconciled
        "412":
          description: Scrappy is not connected
        "502":
          description: Upstream browser error
        "503":
          description: "Either the session is briefly unreachable while Scrappy rebuilds it under the same id (`kind: session_rebuilding` — retry, do not start a new session), or Scrappy could not reach its network exit (`kind: upstream_unavailable`). Both are retryable."
  /api/browser/sessions/{id}/keep-awake:
    post:
      tags:
        - Integrations
      summary: Hold a session open while nobody is watching it
      description: The viewer only pings keepalive while it is open and visible, so a closed or backgrounded tab lets the session idle out. This opts the session into being pinged by the browser-keepalive cron instead, up to a fixed cap. Every minute held awake is a minute of Scrappy runtime metering, so re-post with enabled=false when finished. Upstream's 12-hour absolute cap still applies underneath.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - enabled
              properties:
                enabled:
                  type: boolean
                  description: true holds the session open (extending from now); false releases the hold.
      responses:
        "200":
          description: Hold updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  keepAwakeUntil:
                    type: string
                    format: date-time
                    nullable: true
                    description: When the hold lapses; null when not held.
                  maxMs:
                    type: integer
                    description: The cap
                    in milliseconds.: null
        "400":
          description: enabled (boolean) is required
        "401":
          description: Unauthorized
        "404":
          description: Session not found
        "409":
          description: That session has already ended
  /api/browser/sessions/{id}/scrape-mode:
    post:
      tags:
        - Integrations
      summary: Turn extraction rendering on or off for a live session
      description: "Extraction rendering blocks images, fonts, media and trackers, removes consent walls and unsticks fixed headers, so a page reads cleanly for scraping. A cloud browser session is shared with the human watching it, and they see the same stripped pages with nothing on screen explaining why — so this exists as a human-facing undo that does not require asking the agent to call its own tool. Not gated on browser_sessions_enabled: it spends nothing, acts on an already-live session, and is chiefly a restore. Drivable scope."
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - enabled
              properties:
                enabled:
                  type: boolean
                  description: true renders for extraction; false restores normal rendering. Applied persistently either way, so turning it off also clears the document-start script the agent installed.
      responses:
        "200":
          description: Rendering mode updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  enabled:
                    type: boolean
        "400":
          description: enabled (boolean) is required
        "401":
          description: Unauthorized
        "404":
          description: Session not found
        "409":
          description: That session has ended
        "502":
          description: Upstream browser error
        "503":
          description: "Either the session is briefly unreachable while Scrappy rebuilds it under the same id (`kind: session_rebuilding` — retry, do not start a new session), or Scrappy could not reach its network exit (`kind: upstream_unavailable`). Both are retryable."
  /api/browser/sessions/{id}/navigate:
    post:
      tags:
        - Integrations
      summary: Navigate a session to a URL
      description: http/https only; Scrappy additionally blocks private and internal hosts. Available to anyone who may drive the session.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
              properties:
                url:
                  type: string
      responses:
        "200":
          description: Navigated
        "400":
          description: Missing or non-http(s) URL
        "401":
          description: Unauthorized
        "404":
          description: Session not found
        "412":
          description: Scrappy is not connected
        "502":
          description: Upstream browser error
        "503":
          description: "Either the session is briefly unreachable while Scrappy rebuilds it under the same id (`kind: session_rebuilding` — retry, do not start a new session), or Scrappy could not reach its network exit (`kind: upstream_unavailable`). Both are retryable."
  /api/browser/sessions/{id}/share:
    post:
      tags:
        - Integrations
      summary: Share a browser session with the workspace, or revoke sharing
      description: "Owner-only and revocable at any time. REFUSES a session started from a saved login: sharing one would sign every workspace member in as its owner. A CHECK constraint enforces the same rule at the table."
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - shared
              properties:
                shared:
                  type: boolean
      responses:
        "200":
          description: Sharing updated
        "400":
          description: "`shared` must be a boolean"
        "401":
          description: Unauthorized
        "404":
          description: Session not found or not owned by the caller
        "409":
          description: Session is backed by a saved login and cannot be shared
  /api/browser/profiles:
    get:
      tags:
        - Integrations
      summary: List the caller's saved logins
      description: Owner-scoped with no share path. Returns labels and timestamps only — never cookies, localStorage, or passwords, none of which Kavanah stores.
      responses:
        "200":
          description: Saved logins
        "401":
          description: Unauthorized
    post:
      tags:
        - Integrations
      summary: Create an empty saved-login slot
      description: Holds no login until captured from a live session, which the user must do by signing in themselves.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - label
              properties:
                label:
                  type: string
                siteHint:
                  type: string
      responses:
        "200":
          description: Saved login created
        "400":
          description: Missing label
        "401":
          description: Unauthorized
        "412":
          description: Scrappy is not connected
        "502":
          description: Scrappy rejected the request
  /api/browser/profiles/{id}:
    patch:
      tags:
        - Integrations
      summary: Change a saved login's pinned network exit
      description: "Since 2026-08-24 a capture pins the live exit IP and user agent onto the profile automatically. Send `proxyUrl: null` to clear that pin without touching the login; a string re-pins it. Omitting the key is a 400, not a silent no-op. Upstream self-heals a pin that fails three times in thirty minutes, so this is for a pin that became WRONG rather than dead. Owner-scoped: a profile is only mutable by the member who created it."
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                proxyUrl:
                  type: string
                  nullable: true
                  description: null clears the pin; a string sets it.
      responses:
        "200":
          description: Pin updated
        "400":
          description: proxyUrl absent, or not a string/null
        "401":
          description: Unauthorized
        "404":
          description: Saved login not found
        "412":
          description: Scrappy is not connected
        "502":
          description: Scrappy rejected the request
        "503":
          description: Scrappy was transiently unreachable; retryable
    delete:
      tags:
        - Integrations
      summary: Permanently delete and revoke a saved login
      description: "The revocation path. Upstream is deleted first: losing the local record while the credential survives would leave it live and unreachable through the UI, and therefore unrevokable through it."
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Deleted and revoked
        "401":
          description: Unauthorized
        "404":
          description: Saved login not found
        "412":
          description: Scrappy is not connected
        "502":
          description: Scrappy refused; the saved login was left in place
  /api/browser/profiles/{id}/capture:
    post:
      tags:
        - Integrations
      summary: Save a live session's signed-in state into a saved login
      description: Both the profile and the session must be owned by the caller. Capturing from a merely-shared session would copy its owner's live login, so shared access is not enough. Scrappy rejects an empty capture rather than writing it, so a transient failure cannot wipe a working profile.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sessionId
              properties:
                sessionId:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Login captured
        "400":
          description: Missing sessionId
        "401":
          description: Unauthorized
        "404":
          description: Saved login or session not found
          or not owned by the caller: null
        "412":
          description: Scrappy is not connected
        "502":
          description: Scrappy rejected the capture
  /api/meeting-twin/external/setup:
    get:
      tags:
        - AI Agent
      summary: The caller's AI twin & notetaker setup state
      description: |
        Everything the "AI twin setup" page shows, for the CALLER only: the
        workspace opt-ins (twin in a call, outside meetings, video, notetaker),
        the caller's twin profile (voice, video avatar, what it may talk
        about, auto-join), their likeness consents (never the recordings), and
        per-vendor connection status for Recall.ai, Tavus and ElevenLabs (never
        a key).
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    put:
      tags:
        - AI Agent
      summary: Update the caller's own twin profile
      description: |
        Pick a voice (`voiceId` + `voiceSource` workspace|platform), set
        `knowledge`, `enabled`, `autoRecordExternal`, `autoRecordIncludeInternal`.
        A voice from the workspace ElevenLabs account must not be a CLONE
        unless it is the caller's own consented clone (403); a platform voice
        must be one of Kavanah's stock presets.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
        "403":
          description: Someone else's cloned voice
  /api/meeting-twin/external/vendors/test:
    post:
      tags:
        - AI Agent
      summary: Test a meeting-twin vendor connection
      description: "`{ vendor: recall|tavus|elevenlabs }` → `{ ok, message }` in plain language. Only a vendor 401/403 on the key marks the connection broken."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                vendor:
                  type: string
                  enum:
                    - recall
                    - tavus
                    - elevenlabs
              required:
                - vendor
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
  /api/meeting-twin/external/voices:
    get:
      tags:
        - AI Agent
      summary: Voices the caller may give their twin
      description: The workspace ElevenLabs account's stock/designed voices plus the caller's own consented clone (other people's clones are hidden), and Kavanah's presets when the platform voice service is available.
      responses:
        "200":
          description: Success
        "502":
          description: ElevenLabs refused or was unreachable
  /api/meeting-twin/external/voice-clone:
    post:
      tags:
        - AI Agent
      summary: Clone the caller's own voice (with recorded consent)
      description: |
        Multipart: `typedName` (must equal the caller's profile name),
        `consent` (recording of them reading the consent statement), `samples`
        (1–10 audio files, ≥ ~1 minute in total). The consent (text, SHA-256,
        signature, IP, user agent, and the recording) is stored BEFORE the
        ElevenLabs Instant Voice Clone is created in the WORKSPACE's own
        ElevenLabs account.
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              additionalProperties: true
      responses:
        "201":
          description: Created
        "400":
          description: Invalid request
        "502":
          description: ElevenLabs refused
  /api/meeting-twin/external/face:
    get:
      tags:
        - AI Agent
      summary: The caller's video avatar training status
      responses:
        "200":
          description: Success
    post:
      tags:
        - AI Agent
      summary: Start / finish the video avatar upload
      description: "`{ action: start }` → `{ uploadId }`; then the parts go to `/face/part`; then `{ action: finish, uploadId, parts, contentType, typedName }` records consent and creates the Tavus face."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Upload started
        "201":
          description: Avatar training started
        "400":
          description: Invalid request
        "502":
          description: Tavus refused
  /api/meeting-twin/external/face/part:
    post:
      tags:
        - AI Agent
      summary: Upload one ≤4 MB slice of the avatar training video
      parameters:
        - in: query
          name: uploadId
          required: true
          schema:
            type: string
        - in: query
          name: part
          required: true
          schema:
            type: integer
      requestBody:
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        "200":
          description: Stored
        "400":
          description: Invalid request
  /api/meeting-twin/external/consents:
    post:
      tags:
        - AI Agent
      summary: Withdraw consent to the caller's cloned voice or video avatar
      description: Takes effect immediately; any of the caller's twins in a meeting now leaves.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                kind:
                  type: string
                  enum:
                    - voice_clone
                    - video_face
              required:
                - kind
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
  /api/meeting-twin/external/test:
    post:
      tags:
        - AI Agent
      summary: Test my twin (no meeting)
      description: The caller's twin answers `text` with the outside-meeting hard limits on, returning the reply and its synthesised voice (base64 MP3). Billed to the caller.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                text:
                  type: string
              required:
                - text
      responses:
        "200":
          description: Success
        "400":
          description: Invalid request
  /api/meeting-twin/external/sessions:
    get:
      tags:
        - AI Agent
      summary: The caller's twin / notetaker meetings
      responses:
        "200":
          description: Success
    post:
      tags:
        - AI Agent
      summary: Send the caller's own twin or notetaker to a meeting
      description: |
        `{ meetingUrl, mode: audio|video|notetaker, title?, dealId?, maxMinutes?, scheduledStart?, estimateOnly? }`.
        The rep is always the caller. `estimateOnly: true` returns the cost
        preview, display name and disclosure without creating anything.
        Refused (with a plain-language reason) when the workspace opt-ins are
        off, consent is missing, a vendor key is missing, the link is not a
        Zoom/Meet/Teams/Webex meeting, another twin is already going to that
        meeting (409), or the caller cannot cover the credit preflight (402).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Estimate
        "201":
          description: Sent
        "400":
          description: Not allowed / invalid
        "402":
          description: Not enough AI credits
        "409":
          description: Already going to that meeting
        "502":
          description: A vendor refused
  /api/meeting-twin/external/sessions/{id}:
    get:
      tags:
        - AI Agent
      summary: One twin / notetaker meeting (live status, transcript, summary)
      description: Refreshed from the vendor on read; the rep or a workspace owner/admin.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
        "404":
          description: Not found
    delete:
      tags:
        - AI Agent
      summary: Make a twin / notetaker leave now
      description: The rep, or a workspace owner/admin (kill switch). Never starts anything.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Stopped
        "403":
          description: Not yours
        "404":
          description: Not found
  /api/integrations/recall/webhook:
    post:
      tags:
        - Integrations
      summary: Recall.ai bot status webhook (optional; called by Recall, not by clients)
      description: |
        Optional — status is also fetched by API. Signed-only: Recall signs
        with the workspace verification secret (`webhook-id`,
        `webhook-timestamp`, `webhook-signature`: base64 HMAC-SHA256 of
        `<id>.<timestamp>.<raw body>`; legacy `svix-*` headers accepted). No
        secret pasted ⇒ 401. Unknown bot ⇒ 404.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Applied
        "401":
          description: Missing or bad signature
        "404":
          description: Unknown bot
  /api/integrations/recall/webhook/realtime/{token}:
    post:
      tags:
        - Integrations
      summary: Recall.ai realtime events for one twin/notetaker session (called by Recall)
      description: |
        `transcript.data` and `participant_events.chat_message`, registered per
        bot at dispatch. The per-session secret in the path authenticates the
        session; when the workspace pasted its verification secret, a valid
        Recall signature is also required. Redelivered events are no-ops.
      parameters:
        - in: path
          name: token
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Handled
        "401":
          description: Bad signature
        "404":
          description: Unknown session
  /api/integrations/tavus/webhook/{token}:
    post:
      tags:
        - Integrations
      summary: Tavus conversation callback for one video-twin session (called by Tavus)
      description: Unsigned upstream, so the body is used only as a pointer; every state change is re-read from Tavus's API with the workspace key.
      parameters:
        - in: path
          name: token
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Handled
        "400":
          description: Wrong conversation
        "404":
          description: Unknown session
  /api/integrations/tavus/webhook/training-video/{token}:
    get:
      tags:
        - Integrations
      summary: Avatar training video download for Tavus (token-addressed)
      parameters:
        - in: path
          name: token
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The video
        "404":
          description: Not found
  /api/integrations/tavus/llm/{token}/chat/completions:
    post:
      tags:
        - Integrations
      summary: The video twin's brain — OpenAI-compatible endpoint Tavus calls
      description: Path secret and bearer secret must both match the session. Answers from Kavanah's twin reasoning path with the hard limits applied; streams SSE unless `stream:false`.
      parameters:
        - in: path
          name: token
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Completion
        "401":
          description: Unauthorized
  /api/cron/external-twins:
    post:
      tags:
        - Cron
      summary: Refresh live twins/notetakers, enforce the minute cap, write notes, run notetaker auto-join
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
    get:
      tags:
        - Cron
      summary: Same as POST — Vercel cron invokes this path with GET
      responses:
        "200":
          description: Success
        "401":
          description: Unauthorized
  /api/deals/cadences/{id}/angelsend:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags:
        - Deals
      summary: The native AngelSend campaign(s) behind a cadence
      description: |
        Every mirror of this cadence — one for a network cadence, one per deal
        for a deal cadence — with its status, AngelSend campaign id, channel,
        daily limit, AngelSend sender capacity, the archive copy as last
        VERIFIED on AngelSend, AngelSend's counts and the last problem. Off
        unless an admin turned it on. `deals` lists the deals a deal cadence is
        running on.
      responses:
        "200":
          description: "`{cadenceId, kind, deals, mirrors}`"
        "401":
          description: Unauthorized
        "404":
          description: Cadence not found
    put:
      tags:
        - Deals
      summary: Back a cadence with a native AngelSend campaign, or stop
      description: |
        Admin only, and it runs with the SAVING admin's AngelSend key (re-checked
        as an active admin every run). ON creates the AngelSend campaign from the
        cadence's steps and runs the first sync immediately, which hands every
        enrollment that has not sent yet to AngelSend after checking both
        suppression lists — AngelSend then emails them. OFF pauses the AngelSend
        campaign; enrollments AngelSend never received return to Kavanah's own
        sending. Refused (and the campaign paused) when a step is personalized,
        when AngelSend has no CAN-SPAM company address, or under compliance mode
        when AngelSend cannot be shown to carry the archive BCC. A deal cadence
        needs `dealId`; a network cadence must not have one. `sendChannel`
        `resend` sends on AngelSend's own infrastructure; `gmail` / `outlook`
        send from the admin's own mailbox and add no capacity.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - enabled
              properties:
                enabled:
                  type: boolean
                dealId:
                  type: string
                  format: uuid
                  nullable: true
                sendChannel:
                  type: string
                  enum:
                    - gmail
                    - outlook
                    - resend
                senderAddressId:
                  type: string
                dailyLimit:
                  type: integer
                  minimum: 1
                  maximum: 5000
                handoffInFlight:
                  type: boolean
                  description: |
                    Default false. true also hands enrollments that ALREADY sent
                    on Kavanah to AngelSend at their next step (verified via
                    set-recipient-start-step; a step is never sent twice).
                    Turning the mirror on also clears a sticky missing-archive
                    refusal.
      responses:
        "200":
          description: "`{mirror, sync}` — `sync.refused` says why it will not send, in words to repeat"
        "400":
          description: Invalid input
          a personalized step: null
          or AngelSend not connected: null
        "401":
          description: Unauthorized
        "403":
          description: Not an admin
        "404":
          description: Cadence or deal not found
        "409":
          description: The campaign belongs to another admin's AngelSend account
    post:
      tags:
        - Deals
      summary: Sync a cadence's AngelSend campaign now
      description: |
        Admin only. Removes stopped and suppressed recipients from AngelSend,
        reads sends, replies and unsubscribes back, re-verifies the archive copy
        and hands new enrollments over (which AngelSend then emails).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                dealId:
                  type: string
                  format: uuid
                  nullable: true
      responses:
        "200":
          description: "`{mirror, sync}`"
        "401":
          description: Unauthorized
        "403":
          description: Not an admin
        "404":
          description: Not backed by an AngelSend campaign
  /api/deals/{id}/team/suggest:
    get:
      tags:
        - Deals
      summary: Propose a deal team (read-only)
      description: |
        Ranks the workspace's ACTIVE members for the analyst seat — and for the lead seat when the deal has no active lead — on job title (Analyst/Associate fit the analyst seat, Director/VP/MD/Partner the lead), skill profile, experience against the deal's sector, category, issuance stage and geography, each person's own reported availability, and the open deals and task hours they already carry. Writes nothing and uses no AI. `plan.needed` is how many members the team lacks to reach the workspace minimum; `plan.members` is up to the maximum, best first; `plan.alternates` holds everyone else considered, including unavailable people (never proposed). Needs view access on the deal.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: "`suggestion`: { deal, team, settings, currentMemberCount, plan: { lead, needed, members, alternates } }"
        "400":
          description: No workspace
        "401":
          description: Unauthorized
        "403":
          description: No view access on this deal
        "404":
          description: Deal not found
  /api/deals/{id}/team/staff:
    post:
      tags:
        - Deals
      summary: Confirm a staffing proposal
      description: |
        ADDITIVE: adds each named member (title default "Analyst") and sets the lead only when the deal has no active lead — never removes anyone, never replaces a lead (use PUT /api/deals/{id}/team for that). Ids that are not active members of this workspace come back in `rejected`. The people added are emailed and pushed (the `deals` notification channel) and, when the deal already has a Slack channel / Drive folder and `inviteToSpace` is not false, invited through the CALLER's own Slack and Google connections. Needs edit access on the deal.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                leadUserId:
                  type:
                    - string
                    - "null"
                  format: uuid
                memberUserIds:
                  type: array
                  maxItems: 25
                  items:
                    type: string
                    format: uuid
                title:
                  type: string
                  maxLength: 80
                inviteToSpace:
                  type: boolean
                  default: true
      responses:
        "200":
          description: "`team`, `leadSet`, `added`, `rejected`, `invites`, `notified`"
        "400":
          description: Malformed body or nothing to staff
        "401":
          description: Unauthorized
        "403":
          description: No edit access on this deal
        "404":
          description: Deal not found
  /api/deals/staffing-settings:
    get:
      tags:
        - Deals
      summary: Deal-team staffing settings
      description: |
        `autoStaffEnabled` (default false) — provision-client-space fills a deal's team to `minTeamSize` and picks a lead if it has none before it invites anyone. `minTeamSize` / `maxTeamSize` (defaults 5 and 10, 1–25) bound the automatic fill and the on-demand suggestion. Member-level read.
      responses:
        "200":
          description: "`settings`"
        "401":
          description: Unauthorized
    put:
      tags:
        - Deals
      summary: Update deal-team staffing settings (admin)
      description: |
        Partial patch; an omitted field keeps its value. `autoStaffEnabled` must be a boolean. minTeamSize above maxTeamSize is refused.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                autoStaffEnabled:
                  type: boolean
                minTeamSize:
                  type: integer
                  minimum: 1
                  maximum: 25
                maxTeamSize:
                  type: integer
                  minimum: 1
                  maximum: 25
      responses:
        "200":
          description: "`settings` as saved"
        "400":
          description: Invalid value
        "401":
          description: Unauthorized
        "403":
          description: Not an admin
  /api/deals/issuer-campaigns:
    get:
      tags:
        - Deals
      summary: List issuer campaigns (client prospecting)
      description: |
        Every issuer campaign — a named client mandate the workspace prospects against before there is a deal — with its funnel state (`state`: currentBatch, released, spent, engaged, converted, remainingRanked, exhausted) and cadence. `?includeArchived=1` adds archived campaigns.
      parameters:
        - name: includeArchived
          in: query
          required: false
          schema:
            type: string
            enum:
              - "1"
      responses:
        "200":
          description: "`campaigns`"
        "401":
          description: Unauthorized
    post:
      tags:
        - Deals
      summary: Create an issuer campaign
      description: |
        Sends nothing and arms nothing. Size is scored on a company's raise target; with none recorded its last completed round stands in as a proxy at half weight. A raise below raiseMinDollars is excluded, above raiseMaxDollars is a penalty. `cadence` must be a NETWORK cadence.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  maxLength: 120
                themes:
                  type: array
                  items:
                    type: string
                sectors:
                  type: array
                  items:
                    type: string
                stages:
                  type: array
                  items:
                    type: string
                geographies:
                  type: array
                  items:
                    type: string
                raiseMinDollars:
                  type:
                    - number
                    - "null"
                  minimum: 0
                raiseMaxDollars:
                  type:
                    - number
                    - "null"
                  minimum: 0
                summary:
                  type: string
                cadence:
                  type:
                    - string
                    - "null"
                  description: NETWORK cadence id or exact name
                batchSize:
                  type: integer
                  minimum: 1
                  maximum: 200
                contactsPerFirm:
                  type: integer
                  minimum: 1
                  maximum: 3
                provider:
                  type: string
                  enum:
                    - auto
                    - gmail
                    - angelsend
      responses:
        "201":
          description: "`campaign`"
        "400":
          description: Invalid field
        "401":
          description: Unauthorized
        "409":
          description: A live campaign with that name exists
  /api/deals/issuer-campaigns/{id}:
    get:
      tags:
        - Deals
      summary: One issuer campaign
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: "`campaign`"
        "401":
          description: Unauthorized
        "404":
          description: Not found
    patch:
      tags:
        - Deals
      summary: Edit an issuer campaign's definition
      description: |
        Same fields as create, all optional; an omitted field keeps its value. Sends nothing and resets nobody's status — re-rank to re-score.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: "`campaign`"
        "400":
          description: Invalid field
        "401":
          description: Unauthorized
        "404":
          description: Not found
        "409":
          description: Archived
          or the name is taken: null
  /api/deals/issuer-campaigns/{id}/start:
    post:
      tags:
        - Deals
      summary: Start, top up or resume an issuer campaign
      description: |
        ARMS UNATTENDED OUTBOUND EMAIL from the caller's own mailbox. Ranks if never ranked, releases the next tranche when the current one is fully worked (a re-run while a tranche is open is a top-up that releases nothing), and enrolls the warmest person with an email at each firm in the NETWORK cadence, up to 200 people per call. `autoRelease` must be a boolean; omitted keeps the stored value (default false). A paused campaign resumes by naming its cadence. Refusals are 409 with `code`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                cadence:
                  type: string
                batchSize:
                  type: integer
                  minimum: 1
                  maximum: 200
                contactsPerFirm:
                  type: integer
                  minimum: 1
                  maximum: 3
                autoRelease:
                  type: boolean
                provider:
                  type: string
                  enum:
                    - auto
                    - gmail
                    - angelsend
      responses:
        "200":
          description: "`campaign`, `summary` (lines), `released`, `enrolled`, `sourcingGaps`"
        "400":
          description: Invalid field
        "401":
          description: Unauthorized
        "404":
          description: Not found
        "409":
          description: "Refused: no mandate, no/unusable cadence, inactive sender, nobody qualifies, nothing left to release"
  /api/deals/issuer-campaigns/{id}/pause:
    post:
      tags:
        - Deals
      summary: Pause an issuer campaign
      description: |
        Auto-release off and the cadence detached, so nothing more is released or enrolled automatically. `stopEnrollments: true` also stops the sequences the campaign started and pulls back their queued sends (mail already sent stays sent); `archive: true` archives it.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                stopEnrollments:
                  type: boolean
                archive:
                  type: boolean
      responses:
        "200":
          description: "`campaign`, `enrollments`, `archived`"
        "400":
          description: Invalid field
        "401":
          description: Unauthorized
        "404":
          description: Not found
  /api/deals/issuer-campaigns/{id}/rank:
    post:
      tags:
        - Deals
      summary: Rank issuers against the campaign's mandate
      description: |
        Scores every issuer in the workspace and records the ranking. Contacts nobody, releases nothing, resets nobody's status, uses no AI.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: "`campaign`, `ranked`, `inserted`, `updated`, `rejected` (top 25), `rejectedTotal`"
        "401":
          description: Unauthorized
        "404":
          description: Not found
        "409":
          description: Archived
          or no themes/sectors to rank against: null
  /api/deals/issuer-campaigns/{id}/matches:
    get:
      tags:
        - Deals
      summary: The campaign's ranked prospects
      description: |
        Rank order, each with score (out of 100), reasons, tranche, status (ranked, released, approached, engaged, converted, dismissed), the deal it became, and the firm's raise target / last round (`lastFundingCents` is the size proxy when no raise target is recorded).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - ranked
              - released
              - approached
              - engaged
              - converted
              - dismissed
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 200
      responses:
        "200":
          description: "`matches`"
        "400":
          description: Unknown status
        "401":
          description: Unauthorized
        "404":
          description: Not found
  /api/deals/issuer-campaigns/{id}/matches/{firmId}/convert:
    post:
      tags:
        - Deals
      summary: Convert a prospect into a deal
      description: |
        Creates the deal (caller as owner; default pipeline's first open stage unless told), links every contact at the firm (warmest as primary) and records the match as converted — every issuer ranking in the workspace then excludes the firm. An unknown pipeline is refused, never silently the default.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: firmId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                amountDollars:
                  type:
                    - number
                    - "null"
                  minimum: 0
                pipeline:
                  type: string
                stage:
                  type: string
                expectedClose:
                  type: string
                  format: date
                notes:
                  type: string
      responses:
        "201":
          description: "`deal`, `linkedContacts`, `primaryContactName`, `previousStatus`"
        "400":
          description: Invalid field
          unknown pipeline or stage: null
        "401":
          description: Unauthorized
        "404":
          description: Campaign
          firm or match not found: null
        "409":
          description: Already converted
components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: kavanah_session
      description: JWT session token set on login/signup
    apiKeyBearer:
      type: http
      scheme: bearer
      bearerFormat: kvn_live_…
      description: |
        Workspace API key minted by an admin under Settings → API keys, sent as `Authorization: Bearer kvn_live_…`. Bound to the (workspace, user) that created it; requests act as that user. API-key requests are exempt from the CSRF cookie/header pair that session-cookie requests need. The plaintext token is shown once at creation and cannot be retrieved again.
    appKeyBearer:
      type: http
      scheme: bearer
      bearerFormat: kvn_app_…
      description: |
        Community-app key, minted per install under /apps → Installed → API keys, sent as `Authorization: Bearer kvn_app_…`.
        An app key authenticates as the INSTALLED APP, not as the user who minted it: every RBAC check fails closed, so authorization comes exclusively from the scopes granted at install time. Effective scopes are re-derived on every request as (the key's minted scopes ∩ the install's current granted scopes), so narrowing or disabling an install immediately narrows every key it issued.
        App keys are REJECTED WITH 403 on every operation that does not list this scheme — the callable surface is exactly the operations carrying an `x-app-scope` value. Within that surface an app still sees only what a plain member of the workspace sees (private-project visibility applies), and no DELETE operation is exposed to apps: destructive work goes through an agent tool, where the workspace action policy governs it.
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
      required:
        - error
    LearnItemProgress:
      type: object
      description: One person's progress on one Learn lesson or course introduction.
      properties:
        completed:
          type: boolean
        completedAt:
          type:
            - string
            - "null"
          format: date-time
        autoCompleted:
          type: boolean
          description: Ticked by finishing the narration rather than by the person
        narrationVersion:
          type:
            - string
            - "null"
        positionSeconds:
          type: number
        durationSeconds:
          type:
            - number
            - "null"
        furthestSeconds:
          type: number
        listenedPercent:
          type: number
          description: 0–100; only ever grows
        positionAt:
          type:
            - string
            - "null"
          format: date-time
        readSection:
          type:
            - integer
            - "null"
        readSectionsTotal:
          type:
            - integer
            - "null"
        readAt:
          type:
            - string
            - "null"
          format: date-time
        updatedAt:
          type:
            - string
            - "null"
          format: date-time
    KnowledgeDocument:
      type: object
      description: A Knowledge Library document.
      properties:
        id:
          type: string
        projectRef:
          type:
            - string
            - "null"
        title:
          type: string
        body:
          type: string
          description: Plain-text body (flattened from blocks when blocks are present).
        blocks:
          type: array
          items:
            type: object
        tags:
          type: array
          items:
            type: string
        icon:
          type:
            - string
            - "null"
        cover:
          type:
            - string
            - "null"
        parentId:
          type:
            - string
            - "null"
        isTemplate:
          type: boolean
        templateCategory:
          type:
            - string
            - "null"
        createdByUserId:
          type:
            - string
            - "null"
        updatedByUserId:
          type:
            - string
            - "null"
        agentAuthor:
          type:
            - string
            - "null"
          description: "The AI that last wrote this document through an agent tool (e.g. \"Agentic COO (AI)\", \"Kavanah AI agent\"); null when a person wrote it or it predates provenance (2026-10-12). Provenance, not permission - an agent-written document is never a person's approval. Read-only: never accepted on create or update."
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
          description: Opaque optimistic-lock token; echo as expectedUpdatedAt.
    KnowledgeDocumentDetail:
      allOf:
        - $ref: "#/components/schemas/KnowledgeDocument"
        - type: object
          properties:
            unapprovedAuthority:
              type: array
              description: "Derived on read (single-document GET/PATCH only): sentences that state an authorization (standing authority, a waived approval, silence-as-consent, a delegation) without naming the person who granted it - the same detection the agent's get_knowledge_document label uses. Empty for an ordinary document. Never written back."
              items:
                type: object
                properties:
                  kind:
                    type: string
                    enum:
                      - standing_authority
                      - approval_waived
                      - silence_as_consent
                      - delegated_grant
                  label:
                    type: string
                  sentence:
                    type: string
    OrgTemplateSelection:
      type: object
      properties:
        roles:
          type: array
          items:
            type: string
          description: Role keys to stand up; omitted means all.
        series:
          type: array
          items:
            type: string
          description: Meeting-series keys to create; omitted means all.
        series_models:
          type: object
          additionalProperties:
            type: string
          description: Per-series model (a model id, "auto", or "default").
    SlackManagerInput:
      type: object
      description: An AI Slack manager's writable fields (src/lib/slack-managers-shared.ts validates them for both the settings form and the API). personaId and slackTeamId are required on create.
      properties:
        personaId:
          type: string
          format: uuid
          description: The AI employee that answers. Not a digital twin.
        slackTeamId:
          type: string
          pattern: ^[TE][A-Z0-9]+$
          description: A Slack workspace the owner has connected.
        channelIds:
          type: array
          maxItems: 100
          items:
            type: string
            pattern: ^[CG][A-Z0-9]+$
          description: Channels it answers @mentions in. A manager listing a channel outranks one answering every channel.
        answerAllChannels:
          type: boolean
          description: Answer @mentions in every channel the bot is in.
        answerDms:
          type: boolean
          description: Answer direct messages to the bot (needs the im:history grant).
        playbook:
          type: string
          maxLength: 30000
          description: The only program facts the manager may state.
        escalationUserIds:
          type: array
          maxItems: 10
          items:
            type: string
            format: uuid
          description: Members alerted when the manager hands a question to a person. Empty = the owner. Inactive ids are dropped.
        perPersonHourlyLimit:
          type: integer
          minimum: 1
          maximum: 100
          default: 10
        hourlyReplyCap:
          type: integer
          minimum: 1
          maximum: 5000
          default: 200
        enabled:
          type: boolean
          default: false
    AutomationConfigField:
      type: object
      properties:
        key:
          type: string
        label:
          type: string
        kind:
          type: string
          enum:
            - text
            - textarea
            - number
            - boolean
            - select
            - list
        hint:
          type: string
        required:
          type: boolean
        placeholder:
          type: string
        options:
          type: array
          items:
            type: object
            properties:
              value:
                type: string
              label:
                type: string
    TaskComment:
      type: object
      description: "A task comment. The shape is intentionally just id + body: any PATCH that includes `comments` rebuilds every entry as {id, body}, so extra fields stored here would be dropped on the next board edit."
      properties:
        id:
          type: string
        body:
          type: string
      required:
        - id
        - body
    Task:
      type: object
      properties:
        id:
          type: integer
        workspace_id:
          type: string
          format: uuid
        project_ref:
          type: string
        status_id:
          type: string
        title:
          type: string
        description:
          type: string
        tag:
          type: string
        priority:
          type: string
          enum:
            - Normal
            - Medium
            - High
            - Done
        assigned_to:
          type: string
          nullable: true
        due_date:
          type: string
          format: date
          nullable: true
        reminder_at:
          type: string
          format: date-time
          nullable: true
        reminder_date:
          type: string
          format: date
          nullable: true
        created_by:
          type: string
        subtasks:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              title:
                type: string
              completed:
                type: boolean
        notes:
          type: array
          items:
            type: string
        links:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              url:
                type: string
        comments:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              body:
                type: string
        file_ids:
          type: array
          items:
            type: string
        sort_order:
          type: integer
        created_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
    Client:
      type: object
      properties:
        id:
          type: string
        workspace_id:
          type: string
          format: uuid
        company:
          type: string
        contact_name:
          type: string
        contact_role:
          type: string
        email:
          type: string
          format: email
        location:
          type: string
        website:
          type: string
        logo_url:
          type: string
          nullable: true
        owner_name:
          type: string
        owner_initials:
          type: string
        owner_tone:
          type: string
          enum:
            - sand
            - rose
            - olive
            - slate
            - peach
        stage:
          type: string
          enum:
            - Onboarding
            - Active
            - Expansion
            - Paused
        health:
          type: string
          enum:
            - Healthy
            - Watch
            - At Risk
        invoice_status:
          type: string
          enum:
            - Paid
            - Pending
            - Overdue
        active_projects:
          type: integer
        arr:
          type: number
        last_activity:
          type: string
        next_renewal:
          type: string
          format: date
          nullable: true
        priority:
          type: boolean
        archived:
          type: boolean
        employees:
          type: array
          items:
            type: object
        created_at:
          type: string
          format: date-time
    ProjectWorkspaceBundle:
      type: object
      properties:
        projects:
          type: array
          items:
            type: object
        teamMembers:
          type: array
          items:
            type: object
        workspaceTeams:
          type: array
          items:
            type: object
        notifications:
          type: object
        integrations:
          type: object
    ChatMessage:
      type: object
      properties:
        id:
          type: string
        content:
          type: string
        sender:
          type: string
          enum:
            - me
            - them
        time:
          type: string
        type:
          type: string
          enum:
            - text
            - attachment
            - missed-call
        attachments:
          type: array
          items:
            type: object
    ActivityItem:
      type: object
      properties:
        name:
          type: string
        action:
          type: string
        detail:
          type: string
        tone:
          type: string
          enum:
            - sand
            - rose
            - olive
            - slate
            - peach
        initials:
          type: string
        status:
          type: string
          enum:
            - online
            - busy
            - neutral
    ProfileFormState:
      type: object
      properties:
        firstName:
          type: string
        lastName:
          type: string
        fullName:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
        jobTitle:
          type: string
        department:
          type: string
        bio:
          type: string
        avatarTone:
          type: string
    Swimlane:
      type: object
      properties:
        id:
          type: string
        workspace_id:
          type: string
          format: uuid
        project_ref:
          type: string
        name:
          type: string
        position:
          type: integer
        color:
          type: string
          nullable: true
        archived_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - workspace_id
        - project_ref
        - name
        - position
    ColumnLimit:
      type: object
      properties:
        workspace_id:
          type: string
          format: uuid
        project_ref:
          type: string
        status_id:
          type: string
        wip_limit:
          type: integer
          nullable: true
          minimum: 1
          maximum: 1000
        name_override:
          type: string
          nullable: true
          maxLength: 64
        updated_at:
          type: string
          format: date-time
      required:
        - workspace_id
        - project_ref
        - status_id
    TaskTransition:
      type: object
      properties:
        id:
          type: string
        workspace_id:
          type: string
          format: uuid
        task_id:
          type: integer
        from_status:
          type: string
          nullable: true
        to_status:
          type: string
        moved_by_user_id:
          type: string
          nullable: true
        moved_at:
          type: string
          format: date-time
        ms_in_prev_status:
          type: integer
          nullable: true
          description: Milliseconds the task spent in from_status. Null on the first transition.
      required:
        - id
        - workspace_id
        - task_id
        - to_status
        - moved_at
    StatusTiming:
      type: object
      description: Aggregate cycle-time stats for one status on one project.
      properties:
        status:
          type: string
        count:
          type: integer
        avg_ms:
          type: integer
        p50_ms:
          type: integer
        p90_ms:
          type: integer
      required:
        - status
        - count
        - avg_ms
        - p50_ms
        - p90_ms
    IntegrationProvider:
      type: object
      description: A row in the public integration catalog.
      properties:
        slug:
          type: string
          description: Stable identifier used in URLs (e.g. `gmail`, `slack`).
        name:
          type: string
        description:
          type: string
        category:
          type: string
          description: Free-form group label (e.g. `communication`, `storage`, `calendar`, `project-management`).
        authType:
          type: string
          enum:
            - api_key
            - oauth2
        capabilities:
          type: array
          items:
            type: string
          description: Capability tags surfaced as badges in Settings → Integrations.
        docsUrl:
          type: string
          nullable: true
        baseUrl:
          type: string
          nullable: true
        enabled:
          type: boolean
      required:
        - slug
        - name
        - description
        - category
        - authType
        - capabilities
        - enabled
    IntegrationConnection:
      type: object
      description: A per-user credential row. Never exposes the credential itself.
      properties:
        providerSlug:
          type: string
        status:
          type: string
          enum:
            - connected
            - disconnected
            - error
        lastStatusReason:
          type: string
          nullable: true
        metadata:
          type: object
          description: |
            Provider-specific display metadata: accountEmail, accountName,
            externalWorkspace, scope, expiresAt, etc. Free-form jsonb.
          additionalProperties: true
        connectedAt:
          type: string
          format: date-time
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
      required:
        - providerSlug
        - status
        - connectedAt
  responses:
    BadRequest:
      description: Validation failed
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing or invalid session
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Authenticated but lacking the required permission
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
